diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index edbc9719340957a185bba1acb4476ee79860d612..9c45ac80f264d628c9733a1c882f6321947772fd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,6 +12,28 @@ variables: services: - docker:dind +before_script: + - mkdir -p $HOME/.docker + - | + cat << EOF > $HOME/.docker/config.json + { + "auths": { + "docker.ozg-sh.de": { + "username": "$NEXUS_USER", + "password": "$NEXUS_PASSWORD" + } + }, + "proxies": { + "default": { + "httpProxy": "$HTTP_PROXY", + "httpsProxy": "$HTTPS_PROXY", + "noProxy": "$NO_PROXY" + } + } + } + EOF + - cat ~/.docker/config.json + cache: paths: - .m2/repository/ @@ -22,6 +44,11 @@ stages: - test - publish +.get-version: + before_script: + - export PROJECT_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout $MAVEN_CLI_OPTS | cut -d'-' -f1) + - export PROJECT_ARTIFACTID=$(mvn help:evaluate -Dexpression=project.artifactId -q -DforceStdout $MAVEN_CLI_OPTS) + build: stage: build script: @@ -54,6 +81,17 @@ snapshot-nexus: only: - main +push-merge-request-snapshot-nexus: + stage: publish + before_script: + - !reference [ .get-version, before_script ] + script: + - mvn versions:set -DnewVersion=${PROJECT_VERSION}-MR-${CI_MERGE_REQUEST_IID}-SNAPSHOT $MAVEN_CLI_OPTS + - mvn deploy -Pnexus-deploy $MAVEN_DEPLOY_CLI_OPTS $MAVEN_CLI_OPTS + rules: + - if: $CI_PIPELINE_SOURCE == 'merge_request_event' + when: manual + release-gitlab: stage: publish script: diff --git a/lombok.config b/lombok.config index 92efe13b61eab60013e5ebc2c50e89792775baa2..1d1c35e4338bf63f0abbe65b582b425da916151f 100644 --- a/lombok.config +++ b/lombok.config @@ -4,4 +4,6 @@ lombok.log.log4j.flagUsage = ERROR lombok.data.flagUsage = ERROR lombok.nonNull.exceptionType = IllegalArgumentException lombok.nonNull.flagUsage = ERROR -lombok.addLombokGeneratedAnnotation = true \ No newline at end of file +lombok.addLombokGeneratedAnnotation = true +lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier +lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Value \ No newline at end of file diff --git a/pom.xml b/pom.xml index d7fdd2a3f9f550d3ca4c7fa093eccb2d53c6df03..0daea7fe22ad13a074facfae4a3d9bbefdc0c646 100644 --- a/pom.xml +++ b/pom.xml @@ -18,9 +18,10 @@ <properties> <api-lib.version>0.16.0</api-lib.version> - <nachrichten-manager.version>2.17.0-SNAPSHOT</nachrichten-manager.version> - <openapi-generator.version>7.10.0</openapi-generator.version> + <nachrichten-manager-postfach-interface.version>2.18.0-OZG-4097-OSI2-Anbindung-SNAPSHOT</nachrichten-manager-postfach-interface.version> + <openapi-generator.version>7.11.0</openapi-generator.version> <swagger-parser.version>2.1.23</swagger-parser.version> + <wiremock.version>3.12.0</wiremock.version> <wiremock-spring-boot.version>3.6.0</wiremock-spring-boot.version> </properties> <dependencies> @@ -33,17 +34,28 @@ <dependency> <groupId>de.ozgcloud.nachrichten</groupId> <artifactId>nachrichten-manager-postfach-interface</artifactId> - <version>${nachrichten-manager.version}</version> + <version>${nachrichten-manager-postfach-interface.version}</version> </dependency> - <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-mongodb</artifactId> + </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> + <dependency> + <groupId>net.devh</groupId> + <artifactId>grpc-server-spring-boot-starter</artifactId> + </dependency> + <dependency> + <groupId>net.devh</groupId> + <artifactId>grpc-client-spring-boot-starter</artifactId> + </dependency> <dependency> <groupId>org.mapstruct</groupId> @@ -77,6 +89,7 @@ <scope>test</scope> </dependency> + <!-- commons --> <dependency> <groupId>org.apache.commons</groupId> @@ -139,7 +152,10 @@ <configuration> <inputSpec>${project.basedir}/spec/postfach-api-facade.yaml</inputSpec> <generatorName>java</generatorName> + <generateModelTests>false</generateModelTests> + <generateApiTests>false</generateApiTests> <configOptions> + <!-- https://openapi-generator.tech/docs/generators/java/#config-options --> <sourceFolder>src/gen/java/main</sourceFolder> <serializationLibrary>jackson</serializationLibrary> <library>restclient</library> @@ -147,6 +163,8 @@ <apiPackage>de.ozgcloud.nachrichten.postfach.osiv2.gen.api</apiPackage> <modelPackage>de.ozgcloud.nachrichten.postfach.osiv2.gen.model</modelPackage> <openApiNullable>false</openApiNullable> + <generateBuilders>true</generateBuilders> + <useAbstractionForFiles>true</useAbstractionForFiles> </configOptions> </configuration> </execution> diff --git a/scripts/tag-and-push-vorgang-manager-image.sh b/scripts/tag-and-push-vorgang-manager-image.sh index a9358632df041d70d4331cd3b0bdf4a21afacf75..b530349287ad861e3e65eb8270c8079f2e6c87f6 100755 --- a/scripts/tag-and-push-vorgang-manager-image.sh +++ b/scripts/tag-and-push-vorgang-manager-image.sh @@ -2,7 +2,7 @@ set -e -VERSION=2.22.0-OZG-4094-SNAPSHOT-9 +VERSION=2.23.0-OZG-4097-SNAPSHOT-12 docker tag docker.ozg-sh.de/vorgang-manager:build-latest docker.ozg-sh.de/vorgang-manager:$VERSION docker push docker.ozg-sh.de/vorgang-manager:$VERSION \ No newline at end of file diff --git a/spec/postfach-api-facade.yaml b/spec/postfach-api-facade.yaml index 0414d95e73a7a704396be50ed764b23684c96efe..9a67d1cbd7ec9a71af0f96dfbc0512d6a5abd6aa 100644 --- a/spec/postfach-api-facade.yaml +++ b/spec/postfach-api-facade.yaml @@ -844,7 +844,7 @@ paths: detail: Der Dienst ist zurzeit nicht verfügbar. /MessageExchange/v1/Send/{mailboxId}: post: - operationId: SendMessage + operationId: sendMessage tags: - MessageExchange summary: Sendet eine Nachricht an ein externes oder internes Postfach. @@ -1733,6 +1733,7 @@ paths: description: OK /Quarantine/v1/Upload/{guid}: get: + operationId: getUploadStatus tags: - Quarantine summary: Liefert den Status der Virenprüfung. @@ -1819,6 +1820,7 @@ paths: status: 503 detail: Der Dienst ist zurzeit nicht verfügbar. delete: + operationId: deleteUpload tags: - Quarantine summary: Markiert die Datei zum Löschen @@ -1905,6 +1907,7 @@ paths: detail: Der Dienst ist zurzeit nicht verfügbar. /Quarantine/v1/Upload/Chunked: post: + operationId: uploadChunk tags: - Quarantine summary: Lädt ein Dateistück in die Quarantäne hoch diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachException.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachException.java deleted file mode 100644 index f5187621d37cc7b48e47a1884e0df775636fca86..0000000000000000000000000000000000000000 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachException.java +++ /dev/null @@ -1,9 +0,0 @@ -package de.ozgcloud.nachrichten.postfach.osiv2; - -import de.ozgcloud.common.errorhandling.TechnicalException; - -public class OsiPostfachException extends TechnicalException { - public OsiPostfachException(String msg, Throwable cause) { - super(msg, cause); - } -} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteService.java index 42c45db565ca8bb736d62827193e7d9c498bdbb4..de16ed8fbb8b0295b12c39f5cfed2fd574b17682 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteService.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteService.java @@ -2,49 +2,47 @@ package de.ozgcloud.nachrichten.postfach.osiv2; import java.util.stream.Stream; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Service; - import de.ozgcloud.nachrichten.postfach.PostfachNachricht; import de.ozgcloud.nachrichten.postfach.PostfachRemoteService; -import de.ozgcloud.nachrichten.postfach.osiv2.config.Osi2PostfachProperties; -import de.ozgcloud.nachrichten.postfach.osiv2.transfer.PostfachApiFacadeService; +import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2ExceptionHandler; +import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2PostfachException; +import de.ozgcloud.nachrichten.postfach.osiv2.transfer.Osi2PostfachService; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; -@Service -@ConditionalOnProperty(prefix = Osi2PostfachProperties.PREFIX, name = "enabled", havingValue = "true") +@ServiceIfOsi2Enabled @Log4j2 @RequiredArgsConstructor public class OsiPostfachRemoteService implements PostfachRemoteService { - private final PostfachApiFacadeService postfachApiFacadeService; + private final Osi2PostfachService osi2PostfachService; + private final Osi2ExceptionHandler exceptionHandler; public static final String POSTFACH_TYPE_OSI = "OSI"; @Override public void sendMessage(PostfachNachricht nachricht) { try { - postfachApiFacadeService.sendMessage(nachricht); + osi2PostfachService.sendMessage(nachricht); } catch (RuntimeException e) { - throw new OsiPostfachException("Failed to send message", e); + throw new Osi2PostfachException("sendMessage failed!", exceptionHandler.deriveMessageCode(e), e); } } @Override public Stream<PostfachNachricht> getAllMessages() { try { - return postfachApiFacadeService.receiveMessages(); + return osi2PostfachService.receiveMessages(); } catch (RuntimeException e) { - throw new OsiPostfachException("Failed to get messages", e); + throw new Osi2PostfachException("getAllMessages failed!", exceptionHandler.deriveMessageCode(e), e); } } @Override public void deleteMessage(String messageId) { try { - postfachApiFacadeService.deleteMessage(messageId); + osi2PostfachService.deleteMessage(messageId); } catch (RuntimeException e) { - throw new OsiPostfachException("Failed to delete message", e); + throw new Osi2PostfachException("deleteMessage failed!", exceptionHandler.deriveMessageCode(e), e); } } diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/ServiceIfOsi2Enabled.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/ServiceIfOsi2Enabled.java new file mode 100644 index 0000000000000000000000000000000000000000..a49709999ad359ed7bff2cb00b060fd453cee18b --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/ServiceIfOsi2Enabled.java @@ -0,0 +1,18 @@ +package de.ozgcloud.nachrichten.postfach.osiv2; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import de.ozgcloud.nachrichten.postfach.osiv2.config.Osi2PostfachProperties; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Service +@ConditionalOnProperty(prefix = Osi2PostfachProperties.PREFIX, name = "enabled", havingValue = "true") +public @interface ServiceIfOsi2Enabled { +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/ConcatInputStream.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/ConcatInputStream.java new file mode 100644 index 0000000000000000000000000000000000000000..3ba1da4aa6eb706087181972af2565e465037594 --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/ConcatInputStream.java @@ -0,0 +1,57 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.attachment; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +class ConcatInputStream extends InputStream { + private final Iterator<? extends InputStream> streams; + + private InputStream currentStream = null; + + @Override + public int read() throws IOException { + var buffer = new byte[1]; + var result = read(buffer, 0, 1); + return result < 0 ? result : buffer[0]; + } + + private boolean useNextStreamFails() throws IOException { + close(); + if (streams.hasNext()) { + currentStream = streams.next(); + return false; + } + return true; + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + if (currentStream == null && useNextStreamFails()) { + return -1; + } + var remainingLength = length; + while (remainingLength > 0) { + var readLength = currentStream.read(buffer, offset, remainingLength); + if (readLength > 0) { + remainingLength -= readLength; + offset += readLength; + } else if (useNextStreamFails()) { + break; + } + } + var totalReadLength = length - remainingLength; + return totalReadLength >= 0 ? totalReadLength : -1; + } + + @Override + public void close() throws IOException { + if (currentStream != null) { + currentStream.close(); + currentStream = null; + } + } +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/LimitedInputStream.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/LimitedInputStream.java new file mode 100644 index 0000000000000000000000000000000000000000..430c249c7787fd457170926c408dec0dbe6834e2 --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/LimitedInputStream.java @@ -0,0 +1,38 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.attachment; + +import java.io.IOException; +import java.io.InputStream; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class LimitedInputStream extends InputStream { + private final InputStream parentStream; + private final long limit; + private long readOffset = 0; + + @Override + public int read() throws IOException { + var buffer = new byte[1]; + var result = read(buffer, 0, 1); + return result < 0 ? result : buffer[0]; + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + var remainingLength = limit - readOffset; + if (remainingLength > 0) { + var readLength = parentStream.read(buffer, offset, (int) Math.min(remainingLength, length)); + if (readLength > 0) { + readOffset += readLength; + return readLength; + } + } + return -1; + } + + @Override + public void close() { + // Keep parent stream open + } +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileMapper.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..d6c3f4a650362fb6c33a881d32c03c8289a042c5 --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileMapper.java @@ -0,0 +1,29 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.attachment; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; + +import de.ozgcloud.apilib.file.OzgCloudFile; +import de.ozgcloud.apilib.file.OzgCloudFileId; +import de.ozgcloud.apilib.file.OzgCloudUploadFile; +import de.ozgcloud.nachrichten.postfach.osiv2.model.AttachmentFile; +import de.ozgcloud.vorgang.grpc.file.GrpcOzgFile; + +@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface Osi2AttachmentFileMapper { + + // From de.ozgcloud.nachrichten.file.AttachmentFileService + String ATTACHMENT_FIELD_NAME = "PostfachAttachment"; + + OzgCloudFile mapToUploadFileMetadata(GrpcOzgFile grpcOzgFile); + + default OzgCloudFileId mapToUploadFileId(String fileId) { + return new OzgCloudFileId(fileId); + } + + @Mapping(target = "fileName", source = "name") + @Mapping(target = "fieldName", constant = ATTACHMENT_FIELD_NAME) + OzgCloudUploadFile toOzgCloudUploadFile(AttachmentFile attachmentFile); + +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileService.java new file mode 100644 index 0000000000000000000000000000000000000000..fa0c2c0b0243d43b3bde5c466250db4ac397c993 --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileService.java @@ -0,0 +1,80 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.attachment; + +import static de.ozgcloud.nachrichten.postfach.osiv2.config.Osi2PostfachProperties.*; + +import java.io.InputStream; +import java.util.Iterator; +import java.util.List; + +import org.springframework.beans.factory.annotation.Qualifier; + +import de.ozgcloud.apilib.common.callcontext.OzgCloudCallContextAttachingInterceptor; +import de.ozgcloud.apilib.common.callcontext.OzgCloudCallContextProvider; +import de.ozgcloud.apilib.file.OzgCloudFile; +import de.ozgcloud.apilib.file.OzgCloudFileService; +import de.ozgcloud.nachrichten.postfach.osiv2.ServiceIfOsi2Enabled; +import de.ozgcloud.nachrichten.postfach.osiv2.model.AttachmentFile; +import de.ozgcloud.vorgang.grpc.binaryFile.BinaryFileServiceGrpc; +import de.ozgcloud.vorgang.grpc.binaryFile.GrpcBinaryFilesRequest; +import de.ozgcloud.vorgang.grpc.binaryFile.GrpcGetBinaryFileDataRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import net.devh.boot.grpc.client.inject.GrpcClient; + +@Log4j2 +@ServiceIfOsi2Enabled +@RequiredArgsConstructor +public class Osi2AttachmentFileService { + + private BinaryFileServiceGrpc.BinaryFileServiceBlockingStub binaryFileServiceStub; + private final Osi2AttachmentFileMapper attachmentFileMapper; + @Qualifier(OZG_CLOUD_FILE_SERVICE_NAME) + private final OzgCloudFileService ozgCloudFileService; + private final OzgCloudCallContextProvider ozgCloudCallContextProvider; + + @GrpcClient(GRPC_FILE_MANAGER_NAME) + public void setBinaryFileServiceStub(BinaryFileServiceGrpc.BinaryFileServiceBlockingStub binaryFilServiceStubWithoutInterceptor) { + this.binaryFileServiceStub = binaryFilServiceStubWithoutInterceptor + .withInterceptors(new OzgCloudCallContextAttachingInterceptor(ozgCloudCallContextProvider)); + } + + public List<OzgCloudFile> getFileMetadataOfIds(List<String> fileIds) { + return binaryFileServiceStub.findBinaryFilesMetaData(createRequestWithFileIds(fileIds)) + .getFileList() + .stream() + .map(attachmentFileMapper::mapToUploadFileMetadata) + .toList(); + } + + GrpcBinaryFilesRequest createRequestWithFileIds(List<String> fileIds) { + return GrpcBinaryFilesRequest.newBuilder().addAllFileId(fileIds).build(); + } + + public String uploadFileAndReturnId(AttachmentFile attachmentFile, InputStream fileInputStream) { + var attachmentFileId = ozgCloudFileService.uploadFile( + attachmentFileMapper.toOzgCloudUploadFile(attachmentFile), + fileInputStream + ); + return attachmentFileId.toString(); + } + + public InputStream downloadFileContent(String fileId) { + var response = binaryFileServiceStub.getBinaryFileContent(createRequestWithFileId(fileId)); + return new ConcatInputStream(new Iterator<>() { + @Override + public boolean hasNext() { + return response.hasNext(); + } + + @Override + public InputStream next() { + return response.next().getFileContent().newInput(); + } + }); + } + + GrpcGetBinaryFileDataRequest createRequestWithFileId(String fileId) { + return GrpcGetBinaryFileDataRequest.newBuilder().setFileId(fileId).build(); + } + +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2PersistAttachmentService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2PersistAttachmentService.java new file mode 100644 index 0000000000000000000000000000000000000000..5e56103a281e039cd456c88b50d0020e787f061f --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2PersistAttachmentService.java @@ -0,0 +1,52 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.attachment; + +import java.io.IOException; +import java.util.List; + +import org.springframework.core.io.Resource; + +import de.ozgcloud.nachrichten.postfach.osiv2.model.AttachmentFile; +import de.ozgcloud.nachrichten.postfach.osiv2.ServiceIfOsi2Enabled; +import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2RuntimeException; +import de.ozgcloud.nachrichten.postfach.osiv2.model.AttachmentInfo; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message; +import de.ozgcloud.nachrichten.postfach.osiv2.transfer.PostfachApiFacadeService; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@ServiceIfOsi2Enabled +@RequiredArgsConstructor +public class Osi2PersistAttachmentService { + private final PostfachApiFacadeService postfachApiFacadeService; + private final Osi2AttachmentFileService attachmentFileService; + + public List<String> persistAttachments(Osi2Message osi2Message) { + return osi2Message.attachments().stream() + .map(info -> persistAttachment(osi2Message, info)) + .toList(); + } + + String persistAttachment(Osi2Message osi2Message, AttachmentInfo attachment) { + return persistAttachmentFile( + buildAttachmentFile(osi2Message.vorgangId(), attachment), + postfachApiFacadeService.downloadAttachment(osi2Message.messageId(), attachment.guid()) + ); + } + + AttachmentFile buildAttachmentFile(String vorgangId, AttachmentInfo attachment) { + return AttachmentFile.builder() + .name(attachment.name()) + .contentType(attachment.contentType()) + .vorgangId(vorgangId) + .build(); + } + + String persistAttachmentFile(AttachmentFile attachmentFile, Resource content) { + try (var inputStream = content.getInputStream()) { + return attachmentFileService.uploadFileAndReturnId(attachmentFile, inputStream); + } catch (IOException e) { + throw new Osi2RuntimeException("Error while persisting attachment", e); + } + } +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/ApiClientConfiguration.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/ApiClientConfiguration.java index e9948c36a99efc62d6d03c533da3971a5c515438..6b28b57315c3cce2c26f9e88c6275fafe3f989a4 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/ApiClientConfiguration.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/ApiClientConfiguration.java @@ -11,7 +11,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.converter.FormHttpMessageConverter; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; @@ -31,8 +30,12 @@ import org.springframework.web.client.RestClient; import de.ozgcloud.nachrichten.postfach.osiv2.gen.ApiClient; import de.ozgcloud.nachrichten.postfach.osiv2.gen.api.MessageExchangeApi; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.api.QuarantineApi; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; +@Log4j2 @Configuration @RequiredArgsConstructor @ConditionalOnProperty(prefix = Osi2PostfachProperties.PREFIX, name = "enabled", havingValue = "true") @@ -50,8 +53,15 @@ public class ApiClientConfiguration { } @Bean + QuarantineApi quarantineApi(ApiClient apiClient) { + return new QuarantineApi(apiClient); + } + + @Bean + @SneakyThrows ApiClient apiClient() { var apiClient = new ApiClient(restClient()); + LOG.info("Setting api client base path to {}", apiConfiguration.getUrl()); apiClient.setBasePath(apiConfiguration.getUrl()); return apiClient; } @@ -73,6 +83,7 @@ public class ApiClientConfiguration { private ClientHttpRequestFactory createProxyRequestFactory() { var requestFactory = new HttpComponentsClientHttpRequestFactory(); if (proxyConfiguration.isEnabled()) { + LOG.info("Using proxy configuration: {}:{}", proxyConfiguration.getHost(), proxyConfiguration.getPort()); requestFactory.setHttpClient( HttpClientBuilder.create() .setProxy(new HttpHost(proxyConfiguration.getHost(), proxyConfiguration.getPort())) @@ -88,6 +99,7 @@ public class ApiClientConfiguration { var username = proxyConfiguration.getUsername(); var password = proxyConfiguration.getPassword(); if (username != null && password != null) { + LOG.info("Using proxy authentication with username {}", username); credentialsProvider.setCredentials(new AuthScope(proxyConfiguration.getHost(), proxyConfiguration.getPort()), new UsernamePasswordCredentials(username, password.toCharArray())); } @@ -111,6 +123,8 @@ public class ApiClientConfiguration { } private ClientRegistration osi2ClientRegistration() { + LOG.info("Creating client registration with clientId: {}, tokenUri: {}, scope: {}, resource: {}", authConfiguration.getClientId(), + authConfiguration.getTokenUri(), authConfiguration.getScope(), authConfiguration.getResource()); return ClientRegistration.withRegistrationId(CLIENT_REGISTRATION_ID) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/Osi2PostfachProperties.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/Osi2PostfachProperties.java index f670ec7a5c42c51c5953aa1703ff2485b1beb3aa..e4b53fb59502e8f3afcc3b05066d4a400afb3da6 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/Osi2PostfachProperties.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/Osi2PostfachProperties.java @@ -3,21 +3,25 @@ package de.ozgcloud.nachrichten.postfach.osiv2.config; import java.util.List; import jakarta.annotation.Nullable; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import lombok.Getter; -import lombok.RequiredArgsConstructor; import lombok.Setter; @Getter @Setter @Configuration -@ConditionalOnProperty(prefix = Osi2PostfachProperties.PREFIX, name = "enabled", havingValue = "true") -@RequiredArgsConstructor +@ConfigurationProperties(prefix = Osi2PostfachProperties.PREFIX) public class Osi2PostfachProperties { + // From de.ozgcloud.nachrichten.NachrichtenManagerConfiguration + public static final String GRPC_FILE_MANAGER_NAME = "file-manager"; + // From de.ozgcloud.nachrichten.NachrichtenManagerConfiguration + public static final String OZG_CLOUD_FILE_SERVICE_NAME = "nachrichten_OzgCloudFileService"; public static final String PREFIX = "ozgcloud.osiv2"; @@ -30,10 +34,16 @@ public class Osi2PostfachProperties { public static class AuthConfiguration { public static final String PREFIX = Osi2PostfachProperties.PREFIX + ".auth"; + @NotBlank private String clientId; + @NotBlank private String clientSecret; - private List<String> scope; + @NotNull + @Valid + private List<@NotBlank String> scope; + @NotBlank private String tokenUri; + @NotBlank private String resource; } @@ -44,8 +54,11 @@ public class Osi2PostfachProperties { public static class ApiConfiguration { public static final String PREFIX = Osi2PostfachProperties.PREFIX + ".api"; + @NotBlank private String url; + @NotBlank private String tenant; + @NotBlank private String nameIdentifier; } @@ -62,7 +75,9 @@ public class Osi2PostfachProperties { private boolean enabled; + @NotBlank private String host; + @NotNull private Integer port; @Nullable diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2ExceptionHandler.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2ExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..395f4272265f5695cb06f9a0cd91ae9ddc396949 --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2ExceptionHandler.java @@ -0,0 +1,37 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.exception; + +import jakarta.annotation.Nullable; + +import org.springframework.http.HttpStatusCode; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestClientResponseException; + +import de.ozgcloud.nachrichten.postfach.PostfachMessageCode; +import de.ozgcloud.nachrichten.postfach.osiv2.ServiceIfOsi2Enabled; + +@ServiceIfOsi2Enabled +public class Osi2ExceptionHandler { + + public PostfachMessageCode deriveMessageCode(@Nullable RuntimeException exception) { + if (exception instanceof RestClientResponseException restClientResponseException) { + return deriveMessageCodeFromRestClientResponseException(restClientResponseException); + } + if (exception instanceof ResourceAccessException) { + return PostfachMessageCode.SERVER_CONNECTION_FAILED_MESSAGE_CODE; + } + return PostfachMessageCode.PROCESS_FAILED_MESSAGE_CODE; + } + + PostfachMessageCode deriveMessageCodeFromRestClientResponseException(RestClientResponseException restClientResponseException) { + if (hasStatusNotFound(restClientResponseException)) { + return PostfachMessageCode.SEND_FAILED_UNKNOWN_POSTFACH_ID_MESSAGE_CODE; + } + return PostfachMessageCode.PROCESS_FAILED_MESSAGE_CODE; + } + + private boolean hasStatusNotFound(RestClientResponseException restClientResponseException) { + return restClientResponseException.getStatusCode().isSameCodeAs(HttpStatusCode.valueOf(404)); + } + + +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2PostfachException.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2PostfachException.java new file mode 100644 index 0000000000000000000000000000000000000000..1fae9a09afc9608a8a6f9943dbca0ec320cb1859 --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2PostfachException.java @@ -0,0 +1,10 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.exception; + +import de.ozgcloud.nachrichten.postfach.PostfachException; +import de.ozgcloud.nachrichten.postfach.PostfachMessageCode; + +public class Osi2PostfachException extends PostfachException { + public Osi2PostfachException(String msg, PostfachMessageCode messageCode, Throwable cause) { + super(msg, messageCode, cause); + } +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2RuntimeException.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2RuntimeException.java new file mode 100644 index 0000000000000000000000000000000000000000..6f1966d63bfb58c42ac643afec10ca9c4ebb336b --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2RuntimeException.java @@ -0,0 +1,12 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.exception; + +public class Osi2RuntimeException extends RuntimeException { + public Osi2RuntimeException(String msg) { + super(msg); + } + + public Osi2RuntimeException(String msg, Throwable cause) { + super(msg, cause); + } + +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..10b9f2a97c575b102427ead2234cd3e77e244b3a --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2UploadException.java @@ -0,0 +1,13 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.exception; + +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.QuarantineStatus; + +public class Osi2UploadException extends Exception { + public Osi2UploadException(QuarantineStatus uploadStatus) { + super(uploadStatus.toString()); + } + + public boolean isUnsafe() { + return QuarantineStatus.fromValue(getMessage()) == QuarantineStatus.UNSAFE; + } +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/AttachmentFile.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/AttachmentFile.java new file mode 100644 index 0000000000000000000000000000000000000000..2486622be46ceabda290edd6bd8b3c52503d5c3c --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/AttachmentFile.java @@ -0,0 +1,10 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.model; +import lombok.Builder; + +@Builder +public record AttachmentFile( + String name, + String contentType, + String vorgangId +) { +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/AttachmentInfo.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/AttachmentInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..5d204b1c0991b12a394199b99919521492c14b1c --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/AttachmentInfo.java @@ -0,0 +1,12 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.model; + +import lombok.Builder; + +@Builder +public record AttachmentInfo( + String guid, + String name, + String contentType, + Long size +) { +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkInfo.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..a119cbaf62538ee51a8f5c30e680936a06d207f8 --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkInfo.java @@ -0,0 +1,41 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.model; + +import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment.*; + +import java.io.InputStream; + +import org.springframework.core.io.AbstractResource; + +import de.ozgcloud.nachrichten.postfach.osiv2.attachment.LimitedInputStream; +import lombok.Builder; + +@Builder +public record FileChunkInfo( + Osi2Attachment upload, + long chunkIndex +) { + public AbstractResource createUploadResource(InputStream fileInputStream) { + return new AbstractResource() { + @Override + public String getDescription() { + return "File chunk " + chunkIndex + " of " + upload.getLoggableString(); + } + + @Override + public InputStream getInputStream() { + return new LimitedInputStream(fileInputStream, contentLength()); + } + + @Override + public long contentLength() { + return chunkIndex == upload.numberOfChunks() ? 0 : getChunkContentLength(); + } + + private long getChunkContentLength() { + return chunkIndex == upload.numberOfChunks() - 1 + ? upload.file().getSize() % CHUNK_SIZE + : CHUNK_SIZE; + } + }; + } +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2Attachment.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2Attachment.java new file mode 100644 index 0000000000000000000000000000000000000000..dc41ec7685a33e83990ce954b9097e523dfe57cc --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2Attachment.java @@ -0,0 +1,29 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.model; + +import java.util.UUID; + +import de.ozgcloud.apilib.file.OzgCloudFile; +import lombok.Builder; + +@Builder +public record Osi2Attachment( + String guid, + OzgCloudFile file +) { + public static final long CHUNK_SIZE = 100L * (2L << 10); + + public static Osi2Attachment from(OzgCloudFile file) { + return Osi2Attachment.builder() + .guid(UUID.randomUUID().toString()) + .file(file) + .build(); + } + + public long numberOfChunks() { + return (file.getSize() + CHUNK_SIZE - 1) / CHUNK_SIZE; + } + + public String getLoggableString() { + return String.format("Attachment(messageGuid=%s, ozgFileId=%s, size=%d)", guid, file.getId(), file.getSize()); + } +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2Message.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2Message.java new file mode 100644 index 0000000000000000000000000000000000000000..f527fceac71309d1bc39f427e1e8e08f367ce89c --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2Message.java @@ -0,0 +1,21 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.model; + +import java.time.ZonedDateTime; +import java.util.List; + +import de.ozgcloud.nachrichten.postfach.PostfachAddress; +import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import lombok.Builder; + +@Builder +public record Osi2Message( + String vorgangId, + PostfachAddress postfachAddress, + String messageId, + ZonedDateTime createdAt, + String subject, + String mailBody, + PostfachNachricht.ReplyOption replyOption, + List<AttachmentInfo> attachments +) { +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2MessageMapper.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2MessageMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..7d81f6f6a4a39d30952f08314ce8204ea72d6562 --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2MessageMapper.java @@ -0,0 +1,26 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.transfer; + +import java.util.List; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; + +import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message; + +@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR) +public interface Osi2MessageMapper { + @Mapping(target = "id", ignore = true) + @Mapping(target = "referencedNachricht", ignore = true) + + @Mapping(target = "createdBy", ignore = true) + + @Mapping(target = "sentAt", ignore = true) + @Mapping(target = "sentSuccessful", ignore = true) + @Mapping(target = "messageCode", ignore = true) + + @Mapping(target = "attachments", source = "attachmentIds") + @Mapping(target = "direction", constant = "IN") + PostfachNachricht toPostfachNachricht(Osi2Message osi2Message, List<String> attachmentIds); +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachService.java new file mode 100644 index 0000000000000000000000000000000000000000..754a9458fcf09d45e7ea55bd5c18e574b10de913 --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachService.java @@ -0,0 +1,45 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.transfer; + +import java.util.stream.Stream; + +import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import de.ozgcloud.nachrichten.postfach.osiv2.ServiceIfOsi2Enabled; +import de.ozgcloud.nachrichten.postfach.osiv2.attachment.Osi2PersistAttachmentService; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@ServiceIfOsi2Enabled +@RequiredArgsConstructor +public class Osi2PostfachService { + private final PostfachApiFacadeService postfachApiFacadeService; + private final Osi2QuarantineService quarantineService; + private final Osi2PersistAttachmentService persistAttachmentService; + private final Osi2MessageMapper messageMapper; + + public void sendMessage(PostfachNachricht nachricht) { + postfachApiFacadeService.sendMessage( + nachricht, + quarantineService.uploadAttachments(nachricht.getAttachments()) + ); + } + + public Stream<PostfachNachricht> receiveMessages() { + return postfachApiFacadeService.fetchPendingMessageIds() + .stream() + .map(this::fetchPostfachNachricht); + } + + PostfachNachricht fetchPostfachNachricht(String messageGuid) { + var message = postfachApiFacadeService.fetchMessageById(messageGuid); + return messageMapper.toPostfachNachricht( + message, + persistAttachmentService.persistAttachments(message) + ); + } + + public void deleteMessage(final String messageId) { + postfachApiFacadeService.deleteMessage(messageId); + } + +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..b36dc7f60e6cbf9c937d8e32c49463eff1042b79 --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java @@ -0,0 +1,119 @@ +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.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.attachment.Osi2AttachmentFileService; +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.Osi2Attachment; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@ServiceIfOsi2Enabled +@RequiredArgsConstructor +public class Osi2QuarantineService { + private final PostfachApiFacadeService postfachApiFacadeService; + private final Osi2AttachmentFileService binaryFileService; + + static final Duration POLLING_INTERVAL = Duration.ofSeconds(1); + static final Duration POLLING_TIMEOUT = Duration.ofMinutes(5); + + public List<Osi2Attachment> uploadAttachments(List<String> attachmentIds) { + return uploadFiles(binaryFileService.getFileMetadataOfIds(attachmentIds)); + } + + List<Osi2Attachment> uploadFiles(List<OzgCloudFile> ozgCloudFiles) { + var sortedUploadFiles = deriveSortedUploadFiles(ozgCloudFiles); + try { + tryUploadSortedFiles(sortedUploadFiles); + return sortedUploadFiles; + } catch (RuntimeException e) { + deleteAttachments(sortedUploadFiles); + throw e; + } + } + + List<Osi2Attachment> deriveSortedUploadFiles(List<OzgCloudFile> ozgCloudFiles) { + return ozgCloudFiles.stream() + .sorted(Comparator.comparing(OzgCloudFile::getSize).reversed()) + .map(Osi2Attachment::from) + .toList(); + } + + void tryUploadSortedFiles(List<Osi2Attachment> sortedUploadFiles) { + uploadFilesToQuarantine(sortedUploadFiles); + waitForVirusScan(sortedUploadFiles); + } + + void uploadFilesToQuarantine(List<Osi2Attachment> uploads) { + uploads.forEach(this::uploadFileToQuarantine); + } + + void uploadFileToQuarantine(Osi2Attachment upload) { + try (var fileInputStream = binaryFileService.downloadFileContent(upload.file().getId().toString())) { + uploadInputStreamToQuarantine(upload, fileInputStream); + } catch (IOException e) { + throw new Osi2RuntimeException("Failed to close upload input stream!", e); + } + } + + void uploadInputStreamToQuarantine(Osi2Attachment upload, InputStream fileInputStream) { + streamFileChunkInfos(upload) + .forEachOrdered(chunkInfo -> + postfachApiFacadeService.uploadChunk( + chunkInfo, + chunkInfo.createUploadResource(fileInputStream) + ) + ); + } + + Stream<FileChunkInfo> streamFileChunkInfos(Osi2Attachment upload) { + return LongStream.range(0, upload.numberOfChunks() + 1) + .mapToObj(chunkIndex -> buildFileChunkInfo(upload, chunkIndex)); + } + + private FileChunkInfo buildFileChunkInfo(Osi2Attachment upload, long chunkIndex) { + return FileChunkInfo.builder() + .upload(upload) + .chunkIndex(chunkIndex) + .build(); + } + + void waitForVirusScan(List<Osi2Attachment> osi2FileMetadata) { + if (!waitUntil(() -> checkVirusScanCompleted(osi2FileMetadata), POLLING_INTERVAL, POLLING_TIMEOUT)) { + throw new Osi2RuntimeException("Expect the scan to complete after %d seconds!".formatted(POLLING_TIMEOUT.getSeconds())); + } + } + + synchronized boolean checkVirusScanCompleted(List<Osi2Attachment> osi2FileMetadata) { + return osi2FileMetadata.stream() + .allMatch(this::checkOneVirusScanCompleted); + } + + boolean checkOneVirusScanCompleted(Osi2Attachment osi2FileMetadata) { + try { + return postfachApiFacadeService.checkUploadSuccessful(osi2FileMetadata.guid()); + } catch (Osi2UploadException e) { + throw new Osi2RuntimeException("%s failed!".formatted(osi2FileMetadata.getLoggableString()), e); + } + } + + void deleteAttachments(List<Osi2Attachment> uploads) { + uploads.stream() + .map(Osi2Attachment::guid) + .forEach(postfachApiFacadeService::deleteFileUpload); + } + +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapper.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapper.java index c2aab079e54ede89a9b32c09d5791e663283b71f..86aa86d13ffd80d919a5f4c8b92ae31c4d39bf22 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapper.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapper.java @@ -2,7 +2,11 @@ package de.ozgcloud.nachrichten.postfach.osiv2.transfer; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -11,30 +15,55 @@ import org.mapstruct.ReportingPolicy; import de.ozgcloud.nachrichten.postfach.PostfachAddress; import de.ozgcloud.nachrichten.postfach.PostfachAddressIdentifier; import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.DomainChunkMetaData; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeFiles; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.OutSendMessageRequestV2; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1References; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyBehavior; +import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment; @Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR) public interface Osi2RequestMapper { - @Mapping(target = "sequencenumber", source = "vorgangId") - @Mapping(target = "body", source = "mailBody") + int MAX_NUMBER_RECEIVED_MESSAGES = 100; + + @Mapping(target = "sequencenumber", source = "nachricht.vorgangId") + @Mapping(target = "body", source = "nachricht.mailBody") @Mapping(target = "displayName", ignore = true) @Mapping(target = "originSender", ignore = true) - @Mapping(target = "replyAction", source = "replyOption") + @Mapping(target = "replyAction", source = "nachricht.replyOption") @Mapping(target = "eidasLevel", constant = "LOW") @Mapping(target = "isObligatory", expression = "java( false )") @Mapping(target = "isHtml", expression = "java( false )") - @Mapping(target = "files", expression = "java( mapMessageExchangeFiles() )") + @Mapping(target = "files", expression = "java( mapMessageExchangeFiles(nachricht, files) )") @Mapping(target = "references", expression = "java( mapReferences() )") - OutSendMessageRequestV2 mapOutSendMessageRequestV2(PostfachNachricht nachricht); + OutSendMessageRequestV2 mapOutSendMessageRequestV2(PostfachNachricht nachricht, List<Osi2Attachment> files); - default List<MessageExchangeFiles> mapMessageExchangeFiles() { - return Collections.emptyList(); + default List<MessageExchangeFiles> mapMessageExchangeFiles(PostfachNachricht nachricht, List<Osi2Attachment> files) { + var filesById = associateUploadWithAttachmentId(files); + return nachricht.getAttachments() + .stream() + .map(fileId -> Objects.requireNonNull( + filesById.get(fileId), + "Expect all attachmentIds are uploaded!" + )) + .map(this::mapMessageExchangeFile) + .toList(); + } + + default Map<String, Osi2Attachment> associateUploadWithAttachmentId(List<Osi2Attachment> uploads) { + return uploads.stream() + .filter(upload -> upload.file() != null && upload.file().getId() != null) + .collect(Collectors.toMap(upload -> upload.file().getId().toString(), Function.identity())); } + @Mapping(target = "mimeType", source = "file.contentType") + @Mapping(target = "name", source = "file.name") + @Mapping(target = "size", source = "file.size") + @Mapping(target = "isOriginalMessage", expression = "java( false )") + MessageExchangeFiles mapMessageExchangeFile(Osi2Attachment fileUpload); + default List<V1References> mapReferences() { return Collections.emptyList(); } @@ -47,13 +76,19 @@ public interface Osi2RequestMapper { }; } + @Mapping(target = "target", constant = "UNSPECIFIED") + @Mapping(target = "uploadUid", source = "upload.guid") + @Mapping(target = "fileName", source = "upload.file.name") + @Mapping(target = "contentType", source = "upload.file.contentType") + @Mapping(target = "totalChunks", expression = "java( (int) fileChunkInfo.upload().numberOfChunks() )") + @Mapping(target = "totalFileSize", source = "upload.file.size") + DomainChunkMetaData mapDomainChunkMetaData(FileChunkInfo fileChunkInfo); + default String mapMailboxId(PostfachNachricht nachricht) { return Optional.ofNullable(nachricht.getPostfachAddress()) .map(PostfachAddress::getIdentifier) - .filter(PostfachAddressIdentifier::isStringBasedIdentifier) - .map(Object::toString) - .orElseThrow(() -> new IllegalArgumentException( - "Missing MailboxId! Expect MailboxId to be a string-based PostfachAddress of PostfachNachricht.")); + .map(PostfachAddressIdentifier::getStringRepresentation) + .orElseThrow(() -> new IllegalArgumentException("Missing MailboxId!")); } } 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 ef8d1ea7f2d5e9d28499e8f05badc783b0854fc5..74a34e0487b9e48ef2ee046d8a5f1ac707778fe6 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 @@ -2,6 +2,10 @@ package de.ozgcloud.nachrichten.postfach.osiv2.transfer; import java.time.OffsetDateTime; import java.time.ZonedDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.UUID; import org.mapstruct.Mapper; @@ -10,13 +14,21 @@ import org.mapstruct.Named; import org.mapstruct.ReportingPolicy; import de.ozgcloud.nachrichten.postfach.PostfachAddress; -import de.ozgcloud.nachrichten.postfach.PostfachAddressIdentifier; import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import de.ozgcloud.nachrichten.postfach.StringBasedIdentifier; import de.ozgcloud.nachrichten.postfach.osiv2.OsiPostfachRemoteService; +import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2RuntimeException; +import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2UploadException; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeReceiveAttachment; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeReceiveMessage; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeReceiveMessagesResponse; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.QuarantineFileResult; +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.V1ReplyFiles; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyMessage; -import lombok.Builder; -import lombok.Getter; +import de.ozgcloud.nachrichten.postfach.osiv2.model.AttachmentInfo; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message; @Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR, imports = { Osi2HtmlDocument.class }) public interface Osi2ResponseMapper { @@ -24,26 +36,18 @@ public interface Osi2ResponseMapper { String POSTFACH_ADDRESS_VERSION = "2.0"; int POSTFACH_ADDRESS_TYPE = 2; - @Mapping(target = "id", ignore = true) @Mapping(target = "vorgangId", source = "sequencenumber") @Mapping(target = "postfachAddress", source = "messageBox") @Mapping(target = "messageId", source = "guid") - @Mapping(target = "referencedNachricht", ignore = true) - @Mapping(target = "createdAt", source = "responseTime", qualifiedByName = "mapOffsetDateTimeToZoned") - @Mapping(target = "createdBy", ignore = true) - - @Mapping(target = "sentAt", ignore = true) - @Mapping(target = "sentSuccessful", ignore = true) - @Mapping(target = "messageCode", ignore = true) - - @Mapping(target = "direction", constant = "IN") - @Mapping(target = "subject", source = "subject") @Mapping(target = "mailBody", source = ".", qualifiedByName = "mapMailBody") @Mapping(target = "replyOption", source = "replyAction") - @Mapping(target = "attachments", ignore = true) - PostfachNachricht toPostfachNachricht(V1ReplyMessage message); + @Mapping(target = "attachments", source = "files", defaultExpression = "java(List.of())") + Osi2Message toMessage(V1ReplyMessage message); + + @Mapping(target = "contentType", source = "mimeType") + AttachmentInfo toAttachmentInfo(V1ReplyFiles attachment); default String mapNullToEmpty(String value) { return value == null ? "" : value; @@ -69,7 +73,7 @@ public interface Osi2ResponseMapper { .type(POSTFACH_ADDRESS_TYPE) .version(POSTFACH_ADDRESS_VERSION) .identifier(StringBasedIdentifier.builder() - .mailboxId(messageBox.toString()) + .postfachId(messageBox.toString()) .build()) .serviceKontoType(OsiPostfachRemoteService.POSTFACH_TYPE_OSI) .build(); @@ -86,20 +90,41 @@ public interface Osi2ResponseMapper { }; } - @Builder - @Getter - class StringBasedIdentifier implements PostfachAddressIdentifier { + default List<String> toMessageIds(MessageExchangeReceiveMessagesResponse response) { + if (response == null) { + throw new Osi2RuntimeException("Expect non empty response!", null); + } + + return Optional.ofNullable(response.getMessages()) + .stream() + .flatMap(Collection::stream) + .map(MessageExchangeReceiveMessage::getGuid) + .filter(Objects::nonNull) + .map(UUID::toString) + .toList(); + } - private String mailboxId; + default boolean isSafe(QuarantineStatus uploadStatus) throws Osi2UploadException { + return switch (uploadStatus) { + case NONE, UNSAFE, CORRUPT, MISSING -> throw new Osi2UploadException(uploadStatus); + case LEGACY, COMMITED -> false; + case SAFE -> true; + }; + } - @Override - public boolean isStringBasedIdentifier() { - return true; + default void checkChunkUploadSuccess(QuarantineFileResult quarantineFileResult) { + if (!Boolean.TRUE.equals(quarantineFileResult.getSuccess())) { + throw new Osi2RuntimeException( + "Chunk-Upload of file %s failed: %s".formatted(quarantineFileResult.getFileUid(), quarantineFileResult.getError()), null); } + } - @Override - public String toString() { - return mailboxId; - } + @Named("mapMessageExchangeReceiveAttachment") + default String mapMessageExchangeReceiveAttachment(MessageExchangeReceiveAttachment attachment) { + return Optional.ofNullable(attachment) + .map(MessageExchangeReceiveAttachment::getGuid) + .map(UUID::toString) + .orElse(null); } + } 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 e1446a7fe8bdeaf2c4eb7e39b66324650f135314..989ae00cba9f3caa4f564fc9ac37b77e323a6efa 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 @@ -1,59 +1,78 @@ package de.ozgcloud.nachrichten.postfach.osiv2.transfer; -import java.util.Collection; -import java.util.Optional; +import static de.ozgcloud.nachrichten.postfach.osiv2.transfer.Osi2RequestMapper.*; + +import java.util.List; import java.util.UUID; -import java.util.stream.Stream; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Service; +import org.springframework.core.io.AbstractResource; +import org.springframework.core.io.Resource; import de.ozgcloud.nachrichten.postfach.PostfachNachricht; -import de.ozgcloud.nachrichten.postfach.osiv2.OsiPostfachException; +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.model.MessageExchangeReceiveMessage; -import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeReceiveMessagesResponse; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.api.QuarantineApi; +import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @Log4j2 -@Service -@ConditionalOnProperty(prefix = Osi2PostfachProperties.PREFIX, name = "enabled", havingValue = "true") +@ServiceIfOsi2Enabled @RequiredArgsConstructor public class PostfachApiFacadeService { private final MessageExchangeApi messageExchangeApi; - private final Osi2RequestMapper osi2RequestMapper; - private final Osi2ResponseMapper osi2ResponseMapper; - - static final int MAX_NUMBER_RECEIVED_MESSAGES = 100; + private final QuarantineApi quarantineApi; + private final Osi2RequestMapper requestMapper; + private final Osi2ResponseMapper responseMapper; + private final Osi2PostfachProperties.ApiConfiguration apiConfiguration; - public void sendMessage(PostfachNachricht nachricht) { + public void sendMessage(PostfachNachricht nachricht, List<Osi2Attachment> attachments) { messageExchangeApi.sendMessage( - osi2RequestMapper.mapMailboxId(nachricht), - osi2RequestMapper.mapOutSendMessageRequestV2(nachricht) + requestMapper.mapMailboxId(nachricht), + requestMapper.mapOutSendMessageRequestV2(nachricht, attachments) ); } - public Stream<PostfachNachricht> receiveMessages() { - return Optional.ofNullable(receiveMessagesResponse().getMessages()) - .stream() - .flatMap(Collection::stream) - .map(this::fetchMessageByGuid); + public <T extends AbstractResource> void uploadChunk(FileChunkInfo chunkInfo, T chunkResource) { + responseMapper.checkChunkUploadSuccess(quarantineApi.uploadChunk( + requestMapper.mapDomainChunkMetaData(chunkInfo), + apiConfiguration.getTenant(), + apiConfiguration.getNameIdentifier(), + chunkResource + )); } - - private MessageExchangeReceiveMessagesResponse receiveMessagesResponse() { - return Optional.ofNullable(messageExchangeApi.receiveMessages(MAX_NUMBER_RECEIVED_MESSAGES, 0)) - .orElseThrow(() -> new OsiPostfachException("Expect non empty response!", null)); + + public boolean checkUploadSuccessful(final String messageId) throws Osi2UploadException { + return responseMapper.isSafe( + quarantineApi.getUploadStatus(UUID.fromString(messageId)) + ); } - PostfachNachricht fetchMessageByGuid(final MessageExchangeReceiveMessage message) { - var messageReply = messageExchangeApi.getMessage(message.getGuid()); - return osi2ResponseMapper.toPostfachNachricht(messageReply); + public List<String> fetchPendingMessageIds() { + return responseMapper.toMessageIds( + messageExchangeApi.receiveMessages(MAX_NUMBER_RECEIVED_MESSAGES, 0) + ); + } + + public Osi2Message fetchMessageById(final String messageId) { + var messageReply = messageExchangeApi.getMessage(UUID.fromString(messageId)); + return responseMapper.toMessage(messageReply); } public void deleteMessage(final String messageId) { messageExchangeApi.deleteMessage(UUID.fromString(messageId)); } + + public void deleteFileUpload(String fileUploadId) { + quarantineApi.deleteUpload(UUID.fromString(fileUploadId)); + } + + public Resource downloadAttachment(String messageGuid, String attachmentGuid) { + return messageExchangeApi.getMessageAttachment(UUID.fromString(messageGuid), UUID.fromString(attachmentGuid)); + } } 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 0000000000000000000000000000000000000000..5acad335c58b9927e9970f04c4ef2634f22fb9fe --- /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/main/resources/application-local.yml b/src/main/resources/application-local.yml index dcbea65d1bba45b8c4882d109a07064f09581a61..caf68438b3c62bc347f1ca72610a84a8e772d00d 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -2,4 +2,10 @@ logging: level: de.ozgcloud.nachrichten.postfach.osiv2: DEBUG org.springframework.http: DEBUG - org.springframework.web.client: DEBUG \ No newline at end of file + org.springframework.web.client: DEBUG + +grpc: + client: + file-manager: + address: static://127.0.0.1:9090 + negotiationType: PLAINTEXT \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 065fffe83bf04cd7a435ee3d7a96b347b2f9379e..6b2cd38abf23654bce2740cfcbd7ff1ba9d4f324 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,4 @@ spring: jackson: - default-property-inclusion: NON_NULL \ No newline at end of file + default-property-inclusion: NON_NULL + diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/HttpProxyConfig.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/HttpProxyConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..6e89c6e4772d1ba97a49543ed051afc67a9a1f20 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/HttpProxyConfig.java @@ -0,0 +1,32 @@ +package de.ozgcloud.nachrichten.postfach.osiv2; + +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public record HttpProxyConfig( + String host, + String port +) { + + public static Optional<HttpProxyConfig> fromEnv() { + return Optional.ofNullable(System.getenv("HTTP_PROXY")) + .map(HttpProxyConfig::fromEnvVar); + } + + private static HttpProxyConfig fromEnvVar(String httpProxyEnvVar) { + var matcher = matchProxyRegex(httpProxyEnvVar); + return new HttpProxyConfig( + matcher.group(1), + matcher.group(2) + ); + } + + private static Matcher matchProxyRegex(String text) { + var matcher = Pattern.compile("([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+):([0-9]+)").matcher(text); + if (matcher.find()) { + return matcher; + } + throw new IllegalArgumentException("Proxy host and port not found in '%s'".formatted(text)); + } +} 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 94405d692b164bdf5d1d3b3c51974274a314d39b..550d15b32897a7017dd46e2954b9d11379249078 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java @@ -1,11 +1,13 @@ package de.ozgcloud.nachrichten.postfach.osiv2; import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static de.ozgcloud.nachrichten.postfach.osiv2.config.Osi2PostfachProperties.*; import static de.ozgcloud.nachrichten.postfach.osiv2.factory.JwtFactory.*; import static org.assertj.core.api.Assertions.*; -import java.time.OffsetDateTime; +import java.util.List; import java.util.UUID; +import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -20,15 +22,23 @@ import org.springframework.test.context.TestPropertySource; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; +import com.thedeanda.lorem.LoremIpsum; -import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import de.ozgcloud.nachrichten.postfach.PostfachMessageCode; +import de.ozgcloud.nachrichten.postfach.osiv2.attachment.Osi2AttachmentFileService; +import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2PostfachException; +import de.ozgcloud.nachrichten.postfach.osiv2.extension.AttachmentExampleUploadUtil; import de.ozgcloud.nachrichten.postfach.osiv2.extension.Jwt; import de.ozgcloud.nachrichten.postfach.osiv2.extension.OsiMockServerExtension; +import de.ozgcloud.nachrichten.postfach.osiv2.extension.VorgangManagerServerExtension; import de.ozgcloud.nachrichten.postfach.osiv2.factory.JsonUtil; import de.ozgcloud.nachrichten.postfach.osiv2.factory.MessageExchangeReceiveMessagesResponseTestFactory; import de.ozgcloud.nachrichten.postfach.osiv2.factory.MessageExchangeSendMessageResponseTestFactory; import de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.V1ReplyFilesTestFactory; 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 lombok.SneakyThrows; @SpringBootTest(classes = TestApplication.class) @@ -41,10 +51,13 @@ class OsiPostfachRemoteServiceITCase { @RegisterExtension static final OsiMockServerExtension OSI_MOCK_SERVER_EXTENSION = new OsiMockServerExtension(); - private final PostfachNachricht postfachNachricht = PostfachNachrichtTestFactory.create(); + @RegisterExtension + static final VorgangManagerServerExtension VORGANG_MANAGER_SERVER_EXTENSION = new VorgangManagerServerExtension(); @Autowired - private OsiPostfachRemoteService osiPostfachRemoteService; + protected OsiPostfachRemoteService osiPostfachRemoteService; + @Autowired + protected Osi2AttachmentFileService osi2AttachmentFileService; @DynamicPropertySource static void dynamicProperties(DynamicPropertyRegistry registry) { @@ -53,9 +66,11 @@ class OsiPostfachRemoteServiceITCase { registry.add("ozgcloud.osiv2.auth.client-id", () -> CLIENT_ID); registry.add("ozgcloud.osiv2.api.url", OSI_MOCK_SERVER_EXTENSION::getPostfachFacadeUrl); registry.add("ozgcloud.osiv2.auth.resource", () -> RESOURCE_URN); + registry.add("grpc.client." + GRPC_FILE_MANAGER_NAME + ".address", VORGANG_MANAGER_SERVER_EXTENSION::getVorgangManagerAddress); + registry.add("grpc.client." + GRPC_FILE_MANAGER_NAME + ".negotiationType", () -> "PLAINTEXT"); } - private WireMockServer postfachFacadeMockServer; + protected WireMockServer postfachFacadeMockServer; @BeforeEach @SneakyThrows @@ -67,15 +82,8 @@ class OsiPostfachRemoteServiceITCase { @Test @SneakyThrows void shouldSendRequestWithJwt() { - // Stub message send response (MessageExchangeApi::sendMessage) - postfachFacadeMockServer.stubFor(post(urlPathTemplate("/MessageExchange/v1/Send/{mailboxId}")) - .willReturn( - okJsonObj( - MessageExchangeSendMessageResponseTestFactory.create() - .messageId(UUID.randomUUID()) - ) - ) - ); + var postfachNachricht = PostfachNachrichtTestFactory.create(); + mockSendResponse(); osiPostfachRemoteService.sendMessage(postfachNachricht); @@ -87,6 +95,140 @@ class OsiPostfachRemoteServiceITCase { assertThat(jwt.body().read("$.aud", String.class)).isEqualTo(RESOURCE_URN); } + @DisplayName("should send message with attachment") + @Test + void shouldSendMessageWithAttachment() { + var textFileId = AttachmentExampleUploadUtil.uploadTextFile(osi2AttachmentFileService); + postfachFacadeMockServer.stubFor(post(urlPathTemplate("/Quarantine/v1/Upload/Chunked")) + .willReturn(okJsonObj(QuarantineFileResult.builder() + .success(true) + .build())) + ); + postfachFacadeMockServer.stubFor(get(urlPathTemplate("/Quarantine/v1/Upload/{messageGuid}")) + .willReturn(okJsonObj(QuarantineStatus.SAFE)) + ); + var postfachNachrichtWithAttachment = PostfachNachrichtTestFactory.createBuilder() + .attachments(List.of(textFileId)) + .build(); + mockSendResponse(); + + osiPostfachRemoteService.sendMessage(postfachNachrichtWithAttachment); + + var requests = postfachFacadeMockServer.findAll( + postRequestedFor(urlPathTemplate("/Quarantine/v1/Upload/Chunked"))); + assertThat(requests).hasSize(2); + Function<Integer, byte[]> requestBodyBytes = requestIndex -> requests.get(requestIndex).getPart("file").getBody().asBytes(); + // should send file in one chunk + assertThat(requestBodyBytes.apply(0)).isEqualTo(AttachmentExampleUploadUtil.EXAMPLE_TEXT_DATA); + // should send empty chunk to complete transfer + assertThat(requestBodyBytes.apply(1)).isEmpty(); + postfachFacadeMockServer.verify( + exactly(1), + getRequestedFor(urlPathTemplate("/Quarantine/v1/Upload/{messageGuid}")) + ); + postfachFacadeMockServer.verify( + exactly(1), + postRequestedFor(urlPathTemplate("/MessageExchange/v1/Send/{mailboxId}")) + ); + } + + @DisplayName("should throw postfach exception with connection error code") + @Test + void shouldThrowPostfachExceptionWithConnectionErrorCode() { + postfachFacadeMockServer.stop(); + + var postfachNachricht = PostfachNachrichtTestFactory.create(); + + assertThatThrownBy(() -> osiPostfachRemoteService.sendMessage(postfachNachricht)) + .isInstanceOf(Osi2PostfachException.class) + .hasFieldOrPropertyWithValue("messageCode", PostfachMessageCode.SERVER_CONNECTION_FAILED_MESSAGE_CODE); + } + + @DisplayName("should throw postfach exception with unknown postfach id error code") + @Test + void shouldThrowPostfachExceptionWithUnknownPostfachIdErrorCode() { + postfachFacadeMockServer.stubFor(post(urlPathTemplate("/MessageExchange/v1/Send/{mailboxId}")) + .willReturn(notFound()) + ); + var postfachNachricht = PostfachNachrichtTestFactory.create(); + + assertThatThrownBy(() -> osiPostfachRemoteService.sendMessage(postfachNachricht)) + .isInstanceOf(Osi2PostfachException.class) + .hasFieldOrPropertyWithValue("messageCode", PostfachMessageCode.SEND_FAILED_UNKNOWN_POSTFACH_ID_MESSAGE_CODE); + } + + @DisplayName("should delete attachments on upload exception") + @Test + void shouldDeleteAttachmentsOnUploadException() { + var textFileId = AttachmentExampleUploadUtil.uploadTextFile(osi2AttachmentFileService); + postfachFacadeMockServer.stubFor(post(urlPathTemplate("/Quarantine/v1/Upload/Chunked")) + .willReturn(okJsonObj(QuarantineFileResult.builder() + .success(false) + .error("Upload failure") + .build())) + ); + postfachFacadeMockServer.stubFor(delete(urlPathTemplate("/Quarantine/v1/Upload/{messageGuid}")) + .willReturn(ok()) + ); + var postfachNachrichtWithAttachment = PostfachNachrichtTestFactory.createBuilder() + .attachments(List.of(textFileId)) + .build(); + + try { + osiPostfachRemoteService.sendMessage(postfachNachrichtWithAttachment); + } catch (Osi2PostfachException e) { + // ignore + } + + postfachFacadeMockServer.verify( + exactly(1), + deleteRequestedFor(urlPathTemplate("/Quarantine/v1/Upload/{messageGuid}")) + ); + } + + @DisplayName("should delete attachments on scan exception") + @Test + void shouldDeleteAttachmentsOnScanException() { + var textFileId = AttachmentExampleUploadUtil.uploadTextFile(osi2AttachmentFileService); + postfachFacadeMockServer.stubFor(post(urlPathTemplate("/Quarantine/v1/Upload/Chunked")) + .willReturn(okJsonObj(QuarantineFileResult.builder() + .success(true) + .build())) + ); + postfachFacadeMockServer.stubFor(get(urlPathTemplate("/Quarantine/v1/Upload/{messageGuid}")) + .willReturn(okJsonObj(QuarantineStatus.UNSAFE)) + ); + postfachFacadeMockServer.stubFor(delete(urlPathTemplate("/Quarantine/v1/Upload/{messageGuid}")) + .willReturn(ok()) + ); + var postfachNachrichtWithAttachment = PostfachNachrichtTestFactory.createBuilder() + .attachments(List.of(textFileId)) + .build(); + + try { + osiPostfachRemoteService.sendMessage(postfachNachrichtWithAttachment); + } catch (Osi2PostfachException e) { + // ignore + } + + postfachFacadeMockServer.verify( + exactly(1), + deleteRequestedFor(urlPathTemplate("/Quarantine/v1/Upload/{messageGuid}")) + ); + } + + private void mockSendResponse() { + // Stub message send response (MessageExchangeApi::sendMessage) + postfachFacadeMockServer.stubFor(post(urlPathTemplate("/MessageExchange/v1/Send/{mailboxId}")) + .willReturn( + okJsonObj( + MessageExchangeSendMessageResponseTestFactory.create() + .messageId(UUID.randomUUID()) + ) + ) + ); + } + @DisplayName("should receive one messages") @Test @SneakyThrows @@ -96,6 +238,12 @@ class OsiPostfachRemoteServiceITCase { var messageList = osiPostfachRemoteService.getAllMessages().toList(); assertThat(messageList).hasSize(1); + var firstMessage = messageList.getFirst(); + assertThat(firstMessage.getAttachments()).hasSize(1); + var fileMetadata = osi2AttachmentFileService.getFileMetadataOfIds(firstMessage.getAttachments()); + var firstAttachment = fileMetadata.getFirst(); + assertThat(firstAttachment.getName()).isNotBlank(); + assertThat(firstAttachment.getContentType()).isEqualTo("text/plain"); } @DisplayName("should receive two messages") @@ -109,23 +257,37 @@ class OsiPostfachRemoteServiceITCase { assertThat(messageList).hasSize(2); } - private void mockPostfachMessageAndResponse(final String... uuids) { + private void mockPostfachMessageAndResponse(final String... messageGuids) { // Stub message listing response (MessageExchangeApi::receiveMessages) postfachFacadeMockServer.stubFor(get(urlPathEqualTo("/MessageExchange/v1/Receive")) .withQueryParam("take", equalTo("100")) .withQueryParam("skip", equalTo("0")) - .willReturn(okJsonObj(MessageExchangeReceiveMessagesResponseTestFactory.create(uuids))) + .willReturn(okJsonObj(MessageExchangeReceiveMessagesResponseTestFactory.create(messageGuids))) ); - for (String uuid : uuids) { + for (String messageGuid : messageGuids) { + + var attachmentText = LoremIpsum.getInstance().getParagraphs(10, 20); + var attachment = V1ReplyFilesTestFactory.createBuilder() + .guid(UUID.randomUUID()) + .name(LoremIpsum.getInstance().getName() + ".txt") + .build(); // Stub individual response for message (MessageExchangeApi::getMessage) postfachFacadeMockServer.stubFor(get(urlPathTemplate("/MessageExchange/v1/Receive/{messageId}")) - .withPathParam("messageId", equalTo(uuid)) - .willReturn(okJsonObj( - V1ReplyMessageTestFactory.create() - .messageBox(UUID.fromString(uuid)) - .responseTime(OffsetDateTime.now()) + .withPathParam("messageId", equalTo(messageGuid)) + .willReturn(okJsonObj(V1ReplyMessageTestFactory.create() + .guid(UUID.fromString(messageGuid)) + .files(List.of(attachment)) )) ); + // Stub attachment response (MessageExchangeApi::getMessageAttachment) + postfachFacadeMockServer.stubFor(get(urlPathTemplate("/MessageExchange/v1/Receive/{messageId}/Attachment/{attachmentId}")) + .withPathParam("messageId", equalTo(messageGuid)) + .withPathParam("attachmentId", equalTo(attachment.getGuid().toString())) + .willReturn(ok() + .withHeader("Content-Type", "application/octet-stream") + .withBody(attachmentText.getBytes()) + ) + ); } } @@ -151,5 +313,4 @@ class OsiPostfachRemoteServiceITCase { .withPathParam("messageId", equalTo(messageId)) ); } - } diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceRemoteITCase.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceRemoteITCase.java index 4bcdb775d291573e8a3ec8f1d503726ff31b878b..9662f9c2a26bdbb63713f4c9b43d36dcc506e392 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceRemoteITCase.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceRemoteITCase.java @@ -1,21 +1,27 @@ package de.ozgcloud.nachrichten.postfach.osiv2; +import static de.ozgcloud.nachrichten.postfach.osiv2.config.Osi2PostfachProperties.*; import static org.assertj.core.api.Assertions.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.Arrays; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.jupiter.api.extension.RegisterExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; +import de.ozgcloud.nachrichten.postfach.PostfachMessageCode; import de.ozgcloud.nachrichten.postfach.PostfachNachricht; -import de.ozgcloud.nachrichten.postfach.osiv2.factory.DummyStringBasedIdentifier; +import de.ozgcloud.nachrichten.postfach.StringBasedIdentifier; +import de.ozgcloud.nachrichten.postfach.osiv2.attachment.Osi2AttachmentFileService; +import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2PostfachException; +import de.ozgcloud.nachrichten.postfach.osiv2.extension.AttachmentExampleUploadUtil; +import de.ozgcloud.nachrichten.postfach.osiv2.extension.VorgangManagerServerExtension; import de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachAddressTestFactory; import de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory; import lombok.SneakyThrows; @@ -28,36 +34,41 @@ class OsiPostfachRemoteServiceRemoteITCase { @Autowired private OsiPostfachRemoteService osiPostfachRemoteService; + @RegisterExtension + static final VorgangManagerServerExtension VORGANG_MANAGER_SERVER_EXTENSION = new VorgangManagerServerExtension(); + @DynamicPropertySource static void dynamicProperties(DynamicPropertyRegistry registry) { registry.add( "ozgcloud.osiv2.auth.client-secret", () -> System.getenv("SH_STAGE_CLIENT_SECRET") ); + registry.add( + "ozgcloud.osiv2.proxy.enabled", + () -> HttpProxyConfig.fromEnv().map(v -> "true").orElse("false") + ); registry.add( "ozgcloud.osiv2.proxy.host", - () -> matchProxyRegex(System.getenv("HTTP_PROXY")).group(1) + () -> HttpProxyConfig.fromEnv().map(HttpProxyConfig::host).orElse("") ); registry.add( "ozgcloud.osiv2.proxy.port", - () -> matchProxyRegex(System.getenv("HTTP_PROXY")).group(2) + () -> HttpProxyConfig.fromEnv().map(HttpProxyConfig::port).orElse("") ); + registry.add("grpc.client." + GRPC_FILE_MANAGER_NAME + ".address", VORGANG_MANAGER_SERVER_EXTENSION::getVorgangManagerAddress); + registry.add("grpc.client." + GRPC_FILE_MANAGER_NAME + ".negotiationType", () -> "PLAINTEXT"); } - private static Matcher matchProxyRegex(String text) { - var matcher = Pattern.compile("([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+):([0-9]+)").matcher(text); - if (matcher.find()) { - return matcher; - } - throw new IllegalArgumentException("Proxy host and port not found in '%s'".formatted(text)); - } + @Autowired + protected Osi2AttachmentFileService osi2AttachmentFileService; - private PostfachNachricht createNachricht() { + private PostfachNachricht createNachricht(String... attachmentIds) { return PostfachNachrichtTestFactory.createBuilder() .replyOption(PostfachNachricht.ReplyOption.POSSIBLE) + .attachments(Arrays.stream(attachmentIds).toList()) .postfachAddress(PostfachAddressTestFactory.createBuilder() - .identifier(DummyStringBasedIdentifier.builder() - .mailboxId("49b5a7e2-5e60-4baf-8ccf-1f5b94b570f3") + .identifier(StringBasedIdentifier.builder() + .postfachId("49b5a7e2-5e60-4baf-8ccf-1f5b94b570f3") .build()) .build()) .build(); @@ -73,6 +84,26 @@ class OsiPostfachRemoteServiceRemoteITCase { .doesNotThrowAnyException(); } + @DisplayName("should not fail sending message with attachment") + @Test + void shouldNotFailSendingMessageWithAttachment() { + var textFileId = AttachmentExampleUploadUtil.uploadTextFile(osi2AttachmentFileService); + var nachricht = createNachricht(textFileId); + + assertThatCode(() -> osiPostfachRemoteService.sendMessage(nachricht)) + .doesNotThrowAnyException(); + } + + @DisplayName("should throw postfach exception with unknown postfach id error code") + @Test + void shouldThrowPostfachExceptionWithUnknownPostfachIdErrorCode() { + var nachrichtWithUnknownPostfachId = PostfachNachrichtTestFactory.create(); + + assertThatThrownBy(() -> osiPostfachRemoteService.sendMessage(nachrichtWithUnknownPostfachId)) + .isInstanceOf(Osi2PostfachException.class) + .hasFieldOrPropertyWithValue("messageCode", PostfachMessageCode.SEND_FAILED_UNKNOWN_POSTFACH_ID_MESSAGE_CODE); + } + @DisplayName("should receive messages") @Test void shouldReceiveMessages() { diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceTest.java index 685c36f61dd8d1d44afc185da5bf94bf3eb4ee67..5cb953eb50afe7831f1096d7912b8e3fe5f8fe70 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceTest.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceTest.java @@ -1,9 +1,9 @@ package de.ozgcloud.nachrichten.postfach.osiv2; +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.MessageExchangeReceiveMessagesResponseTestFactory.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; -import java.util.UUID; import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; @@ -13,18 +13,23 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; +import de.ozgcloud.nachrichten.postfach.PostfachMessageCode; import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2ExceptionHandler; import de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory; -import de.ozgcloud.nachrichten.postfach.osiv2.transfer.PostfachApiFacadeService; +import de.ozgcloud.nachrichten.postfach.osiv2.transfer.Osi2PostfachService; class OsiPostfachRemoteServiceTest { - @Spy @InjectMocks + @Spy private OsiPostfachRemoteService osiPostfachRemoteService; @Mock - private PostfachApiFacadeService postfachApiFacadeService; + private Osi2PostfachService osi2PostfachService; + + @Mock + private Osi2ExceptionHandler exceptionHandler; private final PostfachNachricht nachricht1 = PostfachNachrichtTestFactory.createBuilder() .subject("Nachricht 1") @@ -33,26 +38,32 @@ class OsiPostfachRemoteServiceTest { .subject("Nachricht 2") .build(); + private final PostfachMessageCode postfachMessageCode = PostfachMessageCode.PROCESS_FAILED_MESSAGE_CODE; + @DisplayName("send message") @Nested - class TestSendMessage { + class TestSendOsi2Message { @DisplayName("should call send message") @Test void shouldCallSendMessage() { - postfachApiFacadeService.sendMessage(nachricht1); + osi2PostfachService.sendMessage(nachricht1); - verify(postfachApiFacadeService).sendMessage(nachricht1); + verify(osi2PostfachService).sendMessage(nachricht1); } @DisplayName("should throw osi postfach exception on runtime exception") @Test void shouldThrowOsiPostfachExceptionOnRuntimeException() { - doThrow(new RuntimeException()).when(postfachApiFacadeService).sendMessage(nachricht1); + var runtimeException = createRuntimeException(); + doThrow(runtimeException).when(osi2PostfachService).sendMessage(nachricht1); + when(exceptionHandler.deriveMessageCode(runtimeException)).thenReturn(postfachMessageCode); assertThatThrownBy(() -> osiPostfachRemoteService.sendMessage(nachricht1)) - .isInstanceOf(OsiPostfachException.class); + .hasFieldOrPropertyWithValue("messageCode", postfachMessageCode) + .hasCause(runtimeException); } + } @DisplayName("get all messages") @@ -62,32 +73,48 @@ class OsiPostfachRemoteServiceTest { @DisplayName("should return postfach messages") @Test void shouldReturnPostfachMessages() { - when(postfachApiFacadeService.receiveMessages()).thenReturn(Stream.of(nachricht1, nachricht2)); + when(osi2PostfachService.receiveMessages()).thenReturn(Stream.of(nachricht1, nachricht2)); var result = osiPostfachRemoteService.getAllMessages(); assertThat(result).containsExactly(nachricht1, nachricht2); } + + @DisplayName("should throw osi postfach exception on runtime exception") + @Test + void shouldThrowOsiPostfachExceptionOnRuntimeException() { + var runtimeException = createRuntimeException(); + doThrow(runtimeException).when(osi2PostfachService).receiveMessages(); + when(exceptionHandler.deriveMessageCode(runtimeException)).thenReturn(postfachMessageCode); + + assertThatThrownBy(() -> osiPostfachRemoteService.getAllMessages()) + .hasFieldOrPropertyWithValue("messageCode", postfachMessageCode) + .hasCause(runtimeException); + } } @DisplayName("delete message") @Nested - class TestDeleteMessage { + class TestDeleteOsi2Message { @DisplayName("should call deleteMessage") @Test void shouldCallDeleteMessage() { - postfachApiFacadeService.deleteMessage(UUID.randomUUID().toString()); + osiPostfachRemoteService.deleteMessage(MESSAGE_ID_1); - verify(postfachApiFacadeService).deleteMessage(any()); + verify(osi2PostfachService).deleteMessage(MESSAGE_ID_1); } @DisplayName("should throw osi postfach exception on runtime exception") @Test void shouldThrowOsiPostfachExceptionOnRuntimeException() { - doThrow(new RuntimeException()).when(postfachApiFacadeService).deleteMessage(UUID.randomUUID().toString()); + var runtimeException = createRuntimeException(); + doThrow(runtimeException).when(osi2PostfachService).deleteMessage(any()); + when(exceptionHandler.deriveMessageCode(runtimeException)).thenReturn(postfachMessageCode); - assertThatThrownBy(() -> osiPostfachRemoteService.deleteMessage(anyString())).isInstanceOf(OsiPostfachException.class); + assertThatThrownBy(() -> osiPostfachRemoteService.deleteMessage(MESSAGE_ID_1)) + .hasFieldOrPropertyWithValue("messageCode", postfachMessageCode) + .hasCause(runtimeException); } } @@ -114,4 +141,9 @@ class OsiPostfachRemoteServiceTest { assertThat(result).isTrue(); } } + + private RuntimeException createRuntimeException() { + return new RuntimeException(); + } + } \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/TestApplication.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/TestApplication.java index afa904e215a5a977eaffd542aadc82e26db44004..7a6df869213fdc6c4a5cbcef3edcc56693b0c7cb 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/TestApplication.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/TestApplication.java @@ -1,9 +1,44 @@ package de.ozgcloud.nachrichten.postfach.osiv2; +import static de.ozgcloud.nachrichten.postfach.osiv2.config.Osi2PostfachProperties.*; + +import org.mapstruct.factory.Mappers; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import de.ozgcloud.apilib.common.callcontext.CallContext; +import de.ozgcloud.apilib.common.callcontext.OzgCloudCallContextProvider; +import de.ozgcloud.apilib.file.OzgCloudFileService; +import de.ozgcloud.apilib.file.grpc.GrpcOzgCloudFileService; +import de.ozgcloud.apilib.file.grpc.OzgCloudFileMapper; +import de.ozgcloud.apilib.vorgang.OzgCloudUserIdMapper; +import de.ozgcloud.vorgang.grpc.binaryFile.BinaryFileServiceGrpc; +import net.devh.boot.grpc.client.inject.GrpcClient; @SpringBootApplication @AutoConfiguration public class TestApplication { + + // From de.ozgcloud.nachrichten.common.grpc.NachrichtenCallContextAttachingInterceptor + public static final String NACHRICHTEN_MANAGER_CLIENT_NAME = "OzgCloud_NachrichtenManager"; + public static final String NACHRICHTEN_MANAGER_SENDER_USER_ID = "system-nachrichten_manager-sender"; + + @GrpcClient(GRPC_FILE_MANAGER_NAME) + private BinaryFileServiceGrpc.BinaryFileServiceBlockingStub fileServiceBlockingStub; + @GrpcClient(GRPC_FILE_MANAGER_NAME) + private BinaryFileServiceGrpc.BinaryFileServiceStub fileServiceAsyncServiceStub; + + @Bean + OzgCloudCallContextProvider ozgCloudCallContextProvider() { + var userIdMapper = Mappers.getMapper(OzgCloudUserIdMapper.class); + return () -> CallContext.builder().clientName(NACHRICHTEN_MANAGER_CLIENT_NAME) + .userId(userIdMapper.toUserId(NACHRICHTEN_MANAGER_SENDER_USER_ID)).build(); + } + + @Bean(OZG_CLOUD_FILE_SERVICE_NAME) + OzgCloudFileService grpcOzgCloudFileService(OzgCloudCallContextProvider contextProvider) { + return new GrpcOzgCloudFileService(fileServiceBlockingStub, fileServiceAsyncServiceStub, contextProvider, + Mappers.getMapper(OzgCloudFileMapper.class)); + } } diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/ConcatInputStreamTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/ConcatInputStreamTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9340fe2ab0ae140e324cf7b1ccff1c2b60b65376 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/ConcatInputStreamTest.java @@ -0,0 +1,78 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.attachment; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.ByteArrayInputStream; +import java.util.stream.Stream; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import lombok.SneakyThrows; + +class ConcatInputStreamTest { + + @DisplayName("should concatenate input streams") + @Test + @SneakyThrows + void shouldConcatenateInputStreams() { + var concatStream = createExampleConcatInputStream(); + + var result = IOUtils.toByteArray(concatStream); + + assertEquals("123456789", new String(result)); + } + + @DisplayName("should read one") + @Test + @SneakyThrows + void shouldReadOne() { + var concatStream = createExampleConcatInputStream(); + + var result = concatStream.read(); + + assertThat(result).isEqualTo('1'); + } + + @DisplayName("should write to buffer with offset") + @Test + @SneakyThrows + void shouldWriteToBufferWithOffset() { + var buffer = new byte[10]; + var concatStream = createExampleConcatInputStream(); + + var result = concatStream.read(buffer, 2, 2); + assertThat(result).isEqualTo(2); + assertThat(buffer).containsExactly(0, 0, '1', '2', 0, 0, 0, 0, 0, 0); + + result = concatStream.read(buffer, 3, 2); + assertThat(result).isEqualTo(2); + assertThat(buffer).containsExactly(0, 0, '1', '3', '4', 0, 0, 0, 0, 0); + + result = concatStream.read(buffer, 4, 2); + assertThat(result).isEqualTo(2); + assertThat(buffer).containsExactly(0, 0, '1', '3', '5', '6', 0, 0, 0, 0); + + result = concatStream.read(buffer, 5, 2); + assertThat(result).isEqualTo(2); + assertThat(buffer).containsExactly(0, 0, '1', '3', '5', '7', '8', 0, 0, 0); + + result = concatStream.read(buffer, 6, 2); + assertThat(result).isEqualTo(1); + assertThat(buffer).containsExactly(0, 0, '1', '3', '5', '7', '9', 0, 0, 0); + + result = concatStream.read(buffer, 7, 2); + assertThat(result).isEqualTo(-1); + assertThat(buffer).containsExactly(0, 0, '1', '3', '5', '7', '9', 0, 0, 0); + } + + private ConcatInputStream createExampleConcatInputStream() { + var streams = Stream.of("1234", "567", "", "89") + .map(s -> new ByteArrayInputStream(s.getBytes())) + .toList(); + return new ConcatInputStream(streams.iterator()); + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/LimitedInputStreamTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/LimitedInputStreamTest.java new file mode 100644 index 0000000000000000000000000000000000000000..880c800901bce17d30e5ab2cd38a21647b2e5206 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/LimitedInputStreamTest.java @@ -0,0 +1,54 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.attachment; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.ByteArrayInputStream; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import lombok.SneakyThrows; + +class LimitedInputStreamTest { + + @DisplayName("should limit input stream") + @Test + @SneakyThrows + void shouldLimitInputStream() { + var inputStream = createExampleLimitedInputStream(); + + var result = IOUtils.toByteArray(inputStream); + + assertEquals("12345", new String(result)); + } + + @DisplayName("should read one") + @Test + @SneakyThrows + void shouldReadOne() { + var inputStream = createExampleLimitedInputStream(); + + var result = inputStream.read(); + + assertEquals('1', result); + } + + @DisplayName("should keep parent stream usable") + @Test + @SneakyThrows + void shouldKeepParentStreamUsable() { + var parentStream = new ByteArrayInputStream("123456789".getBytes()); + var inputStream = new LimitedInputStream(parentStream, 5); + var inputStream2 = new LimitedInputStream(parentStream, 5); + IOUtils.toByteArray(inputStream); + + var result = IOUtils.toByteArray(inputStream2); + + assertEquals("6789", new String(result)); + } + + private LimitedInputStream createExampleLimitedInputStream() { + return new LimitedInputStream(new ByteArrayInputStream("123456789".getBytes()), 5); + } +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileMapperTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a608d356b56dda5fe98074aa19badbc6aa486746 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileMapperTest.java @@ -0,0 +1,108 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.attachment; + +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2FileUploadTestFactory.*; +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory.*; +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mapstruct.factory.Mappers; + +import de.ozgcloud.apilib.file.OzgCloudFile; +import de.ozgcloud.apilib.file.OzgCloudUploadFile; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.AttachmentFileTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.GrpcOzgFileTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.model.AttachmentFile; +import de.ozgcloud.vorgang.grpc.file.GrpcOzgFile; + +class Osi2AttachmentFileMapperTest { + + private final Osi2AttachmentFileMapper mapper = Mappers.getMapper(Osi2AttachmentFileMapper.class); + + @DisplayName("map to upload file metadata") + @Nested + class TestMapToUploadFileMetadata { + + private final GrpcOzgFile grpcOzgFile = GrpcOzgFileTestFactory.create(); + + @DisplayName("should map id") + @Test + void shouldMapId() { + var result = doMapping(); + + assertThat(result.getId()).hasToString(grpcOzgFile.getId()); + } + + @DisplayName("should map name") + @Test + void shouldMapName() { + var result = doMapping(); + + assertThat(result.getName()).isEqualTo(grpcOzgFile.getName()); + } + + @DisplayName("should map contentType") + @Test + void shouldMapContentType() { + var result = doMapping(); + + assertThat(result.getContentType()).isEqualTo(grpcOzgFile.getContentType()); + } + + @DisplayName("should map size") + @Test + void shouldMapSize() { + var result = doMapping(); + + assertThat(result.getSize()).isEqualTo(grpcOzgFile.getSize()); + } + + private OzgCloudFile doMapping() { + return mapper.mapToUploadFileMetadata(grpcOzgFile); + } + } + + @DisplayName("to ozg cloud upload file") + @Nested + class TestToOzgCloudUploadFile { + private final AttachmentFile attachmentFile = AttachmentFileTestFactory.create(); + + @DisplayName("should map fileName") + @Test + void shouldMapFileName() { + var result = doMapping(); + + assertThat(result.getFileName()).isEqualTo(UPLOAD_NAME); + } + + @DisplayName("should map contentType") + @Test + void shouldMapContentType() { + var result = doMapping(); + + assertThat(result.getContentType()).isEqualTo(UPLOAD_CONTENT_TYPE); + } + + @DisplayName("should map fieldName") + @Test + void shouldMapFieldName() { + var result = doMapping(); + + assertThat(result.getFieldName()).isEqualTo(Osi2AttachmentFileMapper.ATTACHMENT_FIELD_NAME); + } + + @DisplayName("should map vorgangId") + @Test + void shouldMapVorgangId() { + var result = doMapping(); + + assertThat(result.getVorgangId()).isEqualTo(VORGANG_ID); + } + + private OzgCloudUploadFile doMapping() { + return mapper.toOzgCloudUploadFile(attachmentFile); + } + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..892e9c22928b505e00ec601a89c010b312168be8 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2AttachmentFileServiceTest.java @@ -0,0 +1,250 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.attachment; + +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.GrpcOzgFileTestFactory.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.List; + +import org.apache.commons.io.IOUtils; +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.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +import com.google.protobuf.ByteString; + +import de.ozgcloud.apilib.common.callcontext.OzgCloudCallContextAttachingInterceptor; +import de.ozgcloud.apilib.common.callcontext.OzgCloudCallContextProvider; +import de.ozgcloud.apilib.file.OzgCloudFile; +import de.ozgcloud.apilib.file.OzgCloudFileId; +import de.ozgcloud.apilib.file.OzgCloudFileService; +import de.ozgcloud.apilib.file.OzgCloudFileTestFactory; +import de.ozgcloud.apilib.file.OzgCloudUploadFile; +import de.ozgcloud.apilib.file.OzgCloudUploadFileTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.model.AttachmentFile; +import de.ozgcloud.vorgang.grpc.binaryFile.BinaryFileServiceGrpc; +import de.ozgcloud.vorgang.grpc.binaryFile.GrpcBinaryFilesRequest; +import de.ozgcloud.vorgang.grpc.binaryFile.GrpcFindFilesResponse; +import de.ozgcloud.vorgang.grpc.binaryFile.GrpcGetBinaryFileDataResponse; +import de.ozgcloud.vorgang.grpc.file.GrpcOzgFile; +import lombok.SneakyThrows; + +class Osi2AttachmentFileServiceTest { + + @Spy + @InjectMocks + private Osi2AttachmentFileService service; + + @Mock + private Osi2AttachmentFileMapper attachmentFileMapper; + + @Mock + private OzgCloudFileService ozgCloudFileService; + + @Mock + private OzgCloudCallContextProvider ozgCloudCallContextProvider; + + @Mock + private BinaryFileServiceGrpc.BinaryFileServiceBlockingStub binaryFileServiceStubWithoutInterceptor; + + @Mock + private BinaryFileServiceGrpc.BinaryFileServiceBlockingStub binaryFileServiceStub; + + @Captor + private ArgumentCaptor<OzgCloudCallContextAttachingInterceptor> interceptorCaptor; + + @BeforeEach + void setup() { + when(binaryFileServiceStubWithoutInterceptor.withInterceptors(interceptorCaptor.capture())).thenReturn(binaryFileServiceStub); + service.setBinaryFileServiceStub(binaryFileServiceStubWithoutInterceptor); + } + + @DisplayName("should set ozg cloud call context provider") + @Test + void shouldSetOzgCloudCallContextProvider() { + assertThat(interceptorCaptor.getValue()) + .extracting("callContextProvider") + .isEqualTo(ozgCloudCallContextProvider); + } + + + @DisplayName("get file metadata of ids") + @Nested + class TestGetFileMetadataOfIds { + + @Mock + private GrpcFindFilesResponse grpcFindFilesResponse; + + @Mock + private GrpcBinaryFilesRequest grpcBinaryFilesRequest; + + @Mock + private GrpcOzgFile grpcOzgFile; + + private final OzgCloudFile ozgCloudFile = OzgCloudFileTestFactory.create(); + + @BeforeEach + void mock() { + doReturn(grpcBinaryFilesRequest).when(service).createRequestWithFileIds(any()); + when(binaryFileServiceStub.findBinaryFilesMetaData(any())).thenReturn(grpcFindFilesResponse); + when(grpcFindFilesResponse.getFileList()).thenReturn(List.of(grpcOzgFile)); + when(attachmentFileMapper.mapToUploadFileMetadata(any())).thenReturn(ozgCloudFile); + } + + @DisplayName("should call createRequestWithFileIds") + @Test + void shouldCallCreateRequestWithFileIds() { + getFileMetadataOfIds(); + + verify(service).createRequestWithFileIds(List.of(FILE_ID)); + } + + @DisplayName("should call findBinaryFilesMetaData") + @Test + void shouldCallFindBinaryFilesMetaData() { + getFileMetadataOfIds(); + + verify(binaryFileServiceStub).findBinaryFilesMetaData(grpcBinaryFilesRequest); + } + + @DisplayName("should call map to upload file metadata") + @Test + void shouldCallMapToUploadFileMetadata() { + getFileMetadataOfIds(); + + verify(attachmentFileMapper).mapToUploadFileMetadata(grpcOzgFile); + } + + @DisplayName("should return") + @Test + void shouldReturn() { + var result = getFileMetadataOfIds(); + + assertThat(result).containsExactly(ozgCloudFile); + } + + private List<OzgCloudFile> getFileMetadataOfIds() { + return service.getFileMetadataOfIds(List.of(FILE_ID)); + } + + } + + @DisplayName("create request with file ids") + @Nested + class TestCreateRequestWithFileIds { + + @DisplayName("should return") + @Test + void shouldReturn() { + var result = service.createRequestWithFileIds(List.of(FILE_ID)); + + assertThat(result.getFileIdList()).containsExactly(FILE_ID); + } + + } + + @DisplayName("upload file and return id") + @Nested + class TestUploadFileAndReturnId { + @Mock + private InputStream inputStream; + + @Mock + private AttachmentFile attachmentFile; + + private final OzgCloudUploadFile ozgCloudUploadFile = OzgCloudUploadFileTestFactory.create(); + + @BeforeEach + void mock() { + when(attachmentFileMapper.toOzgCloudUploadFile(any())).thenReturn(ozgCloudUploadFile); + when(ozgCloudFileService.uploadFile(any(), any())).thenReturn(new OzgCloudFileId(FILE_ID)); + } + + @DisplayName("should call toOzgCloudUploadFile") + @Test + void shouldCallToOzgCloudUploadFile() { + uploadFileAndReturnId(); + + verify(attachmentFileMapper).toOzgCloudUploadFile(attachmentFile); + } + + @DisplayName("should call uploadFile") + @Test + void shouldCallUploadFile() { + uploadFileAndReturnId(); + + verify(ozgCloudFileService).uploadFile(any(), any()); + } + + @DisplayName("should return") + @Test + void shouldReturn() { + var result = uploadFileAndReturnId(); + + assertThat(result).isEqualTo(FILE_ID); + } + + private String uploadFileAndReturnId() { + return service.uploadFileAndReturnId(attachmentFile, inputStream); + } + } + + @DisplayName("download file content") + @Nested + class TestDownloadFileContent { + + @Mock + private GrpcGetBinaryFileDataResponse response1; + + @Mock + private ByteString chunkContent1; + + @Mock + private GrpcGetBinaryFileDataResponse response2; + + @Mock + private ByteString chunkContent2; + + @BeforeEach + void mock() { + when(binaryFileServiceStub.getBinaryFileContent(any())).thenReturn(List.of(response1, response2).iterator()); + when(response1.getFileContent()).thenReturn(chunkContent1); + when(response2.getFileContent()).thenReturn(chunkContent2); + when(chunkContent1.newInput()).thenReturn(new ByteArrayInputStream("abc".getBytes())); + when(chunkContent2.newInput()).thenReturn(new ByteArrayInputStream("def".getBytes())); + } + + + @DisplayName("should return") + @Test + @SneakyThrows + void shouldReturn() { + var result = service.downloadFileContent(FILE_ID); + + var concatBytes = IOUtils.toByteArray(result); + assertThat(new String(concatBytes)).isEqualTo("abcdef"); + } + } + + @DisplayName("create request with file id") + @Nested + class TestCreateRequestWithFileId { + + @DisplayName("should return") + @Test + void shouldReturn() { + var result = service.createRequestWithFileId(FILE_ID); + + assertThat(result.getFileId()).isEqualTo(FILE_ID); + } + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2PersistAttachmentServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2PersistAttachmentServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2fcf40037503cf91d20e4ce5206c5c0518e6d72d --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/attachment/Osi2PersistAttachmentServiceTest.java @@ -0,0 +1,213 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.attachment; + +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.GrpcOzgFileTestFactory.*; +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2FileUploadTestFactory.*; +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory.*; +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.V1ReplyMessageTestFactory.*; +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 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.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.springframework.core.io.Resource; + +import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2RuntimeException; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.AttachmentFileTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.AttachmentInfoTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2MessageTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.model.AttachmentFile; +import de.ozgcloud.nachrichten.postfach.osiv2.model.AttachmentInfo; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message; +import de.ozgcloud.nachrichten.postfach.osiv2.transfer.PostfachApiFacadeService; +import lombok.SneakyThrows; + +class Osi2PersistAttachmentServiceTest { + + @Spy + @InjectMocks + private Osi2PersistAttachmentService service; + + @Mock + private PostfachApiFacadeService postfachApiFacadeService; + + @Mock + private Osi2AttachmentFileService attachmentFileService; + + @DisplayName("persist attachments") + @Nested + class TestPersistAttachments { + + @DisplayName("with no attachments") + @Nested + class TestWithNoAttachments { + private final Osi2Message osi2Message = Osi2MessageTestFactory.create(); + + @DisplayName("should return empty list") + @Test + void shouldReturnEmptyList() { + var result = service.persistAttachments(osi2Message); + + assertThat(result).isEmpty(); + } + + } + + @DisplayName("with two attachments") + @Nested + class TestWithTwoAttachments { + + private final AttachmentInfo attachment1 = AttachmentInfoTestFactory.create(); + private final AttachmentInfo attachment2 = AttachmentInfoTestFactory.createBuilder() + .guid(UPLOAD_GUID2) + .build(); + private final Osi2Message osi2Message = Osi2MessageTestFactory.createBuilder() + .attachments(List.of(attachment1, attachment2)) + .build(); + + @BeforeEach + void mock() { + doReturn(FILE_ID).when(service).persistAttachment(any(), eq(attachment1)); + doReturn(FILE_ID_2).when(service).persistAttachment(any(), eq(attachment2)); + } + + @DisplayName("should call persistAttachment twice") + @Test + void shouldCallPersistAttachmentTwice() { + service.persistAttachments(osi2Message); + + verify(service).persistAttachment(osi2Message, attachment1); + verify(service).persistAttachment(osi2Message, attachment2); + } + + @DisplayName("should return") + @Test + void shouldReturn() { + var result = service.persistAttachments(osi2Message); + + assertThat(result).containsExactly(FILE_ID, FILE_ID_2); + } + } + } + + @DisplayName("persist attachment") + @Nested + class TestPersistAttachment { + private final Osi2Message osi2Message = Osi2MessageTestFactory.create(); + private final AttachmentInfo attachment = AttachmentInfoTestFactory.create(); + private final AttachmentFile attachmentFile = AttachmentFileTestFactory.create(); + + @Mock + private Resource content; + + @BeforeEach + void mock() { + when(postfachApiFacadeService.downloadAttachment(any(), any())).thenReturn(content); + doReturn(attachmentFile).when(service).buildAttachmentFile(any(), any()); + doReturn(FILE_ID).when(service).persistAttachmentFile(any(), any()); + } + + @DisplayName("should call persistAttachmentFile") + @Test + void shouldCallPersistAttachmentFile() { + service.persistAttachment(osi2Message, attachment); + + verify(service).persistAttachmentFile(attachmentFile, content); + } + + @DisplayName("should call buildAttachmentFile") + @Test + void shouldCallBuildAttachmentFile() { + service.persistAttachment(osi2Message, attachment); + + verify(service).buildAttachmentFile(VORGANG_ID, attachment); + } + + @DisplayName("should call downloadAttachment") + @Test + void shouldCallDownloadAttachment() { + service.persistAttachment(osi2Message, attachment); + + verify(postfachApiFacadeService).downloadAttachment(MESSAGE_ID, UPLOAD_GUID); + } + } + + @DisplayName("build attachment file") + @Nested + class TestBuildAttachmentFile { + private final AttachmentInfo attachment = AttachmentInfoTestFactory.create(); + + @DisplayName("should return") + @Test + void shouldReturn() { + var result = service.buildAttachmentFile(VORGANG_ID, attachment); + + assertThat(result).usingRecursiveComparison() + .isEqualTo(AttachmentFileTestFactory.createBuilder() + .vorgangId(VORGANG_ID) + .build()); + } + } + + @DisplayName("persist attachment file") + @Nested + class TestPersistAttachmentFile { + private final AttachmentFile attachmentFile = AttachmentFileTestFactory.create(); + @Mock + private Resource content; + + @Mock + private InputStream inputStream; + + @BeforeEach + @SneakyThrows + void mock() { + when(attachmentFileService.uploadFileAndReturnId(any(), any())).thenReturn(FILE_ID); + when(content.getInputStream()).thenReturn(inputStream); + } + + @DisplayName("should close inputStream") + @Test + @SneakyThrows + void shouldCloseInputStream() { + service.persistAttachmentFile(attachmentFile, content); + + verify(inputStream).close(); + } + + @DisplayName("should call uploadFileAndReturnId") + @Test + void shouldCallUploadFileAndReturnId() { + service.persistAttachmentFile(attachmentFile, content); + + verify(attachmentFileService).uploadFileAndReturnId(attachmentFile, inputStream); + } + + @DisplayName("should rethrow IOException") + @Test + @SneakyThrows + void shouldRethrowIoException() { + doThrow(new IOException("test")).when(inputStream).close(); + + assertThatThrownBy(() -> service.persistAttachmentFile(attachmentFile, content)) + .isInstanceOf(Osi2RuntimeException.class); + } + + @DisplayName("should return") + @Test + void shouldReturn() { + var result = service.persistAttachmentFile(attachmentFile, content); + + assertThat(result).isEqualTo(FILE_ID); + } + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2ExceptionHandlerTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2ExceptionHandlerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..df51704620bc574bcbf1c1f01c4c5ff7786ab3fc --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2ExceptionHandlerTest.java @@ -0,0 +1,76 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.exception; + +import static de.ozgcloud.nachrichten.postfach.PostfachMessageCode.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.springframework.http.HttpStatusCode; +import org.springframework.web.client.RestClientResponseException; + +class Osi2ExceptionHandlerTest { + + @InjectMocks + @Spy + private Osi2ExceptionHandler handler; + + @DisplayName("derive message code") + @Nested + class TestDeriveOsi2MessageCode { + @Mock + RestClientResponseException restClientResponseException; + + @DisplayName("should derive message code from rest client response exception") + @Test + void shouldDeriveMessageCodeFromRestClientResponseException() { + doReturn(SERVER_CONNECTION_FAILED_MESSAGE_CODE).when(handler) + .deriveMessageCodeFromRestClientResponseException(restClientResponseException); + + var result = handler.deriveMessageCode(restClientResponseException); + + assertThat(result).isEqualTo(SERVER_CONNECTION_FAILED_MESSAGE_CODE); + } + + @DisplayName("should return processed failed error code by default") + @Test + void shouldReturnProcessedFailedErrorCodeByDefault() { + var result = handler.deriveMessageCode(null); + + assertThat(result).isEqualTo(PROCESS_FAILED_MESSAGE_CODE); + } + } + + @DisplayName("derive message code from rest client response exception") + @Nested + class TestDeriveOsi2MessageCodeFromRestClientResponseException { + + @Mock + private RestClientResponseException restClientResponseException; + + @DisplayName("should return unknown postfach id error code for 404") + @Test + void shouldReturnUnknownPostfachIdErrorCodeFor404() { + when(restClientResponseException.getStatusCode()).thenReturn(HttpStatusCode.valueOf(404)); + + var result = handler.deriveMessageCodeFromRestClientResponseException(restClientResponseException); + + assertThat(result).isEqualTo(SEND_FAILED_UNKNOWN_POSTFACH_ID_MESSAGE_CODE); + } + + @DisplayName("should return process failed message code by default") + @Test + void shouldReturnProcessFailedMessageCodeByDefault() { + when(restClientResponseException.getStatusCode()).thenReturn(HttpStatusCode.valueOf(500)); + + var result = handler.deriveMessageCodeFromRestClientResponseException(restClientResponseException); + + assertThat(result).isEqualTo(PROCESS_FAILED_MESSAGE_CODE); + } + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/AttachmentExampleUploadUtil.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/AttachmentExampleUploadUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..8aa54916936532edcdc288378702867186469fe6 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/AttachmentExampleUploadUtil.java @@ -0,0 +1,26 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.extension; + +import java.io.ByteArrayInputStream; +import java.util.UUID; + +import com.thedeanda.lorem.LoremIpsum; + +import de.ozgcloud.nachrichten.postfach.osiv2.attachment.Osi2AttachmentFileService; +import de.ozgcloud.nachrichten.postfach.osiv2.model.AttachmentFile; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +public class AttachmentExampleUploadUtil { + + public static final byte[] EXAMPLE_TEXT_DATA = LoremIpsum.getInstance().getParagraphs(5,100).getBytes(); + + public static String uploadTextFile(Osi2AttachmentFileService remoteService) { + + return remoteService.uploadFileAndReturnId(AttachmentFile.builder() + .contentType("text/plain") + .name("test.txt") + .vorgangId(UUID.randomUUID().toString()) + .build(), new ByteArrayInputStream(EXAMPLE_TEXT_DATA)); + } + +} diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/OsiMockServerExtension.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/OsiMockServerExtension.java index 71cd29cbd57f980c253f593a8c1a30f8286e38a4..84d367b808ca8a5fad1de979f57d46b32840351b 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/OsiMockServerExtension.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/OsiMockServerExtension.java @@ -3,6 +3,10 @@ package de.ozgcloud.nachrichten.postfach.osiv2.extension; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static de.ozgcloud.nachrichten.postfach.osiv2.factory.JwtFactory.*; +import java.util.Optional; + +import jakarta.annotation.Nullable; + import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; @@ -19,7 +23,7 @@ import lombok.extern.log4j.Log4j2; @Log4j2 @Getter @RequiredArgsConstructor -public class OsiMockServerExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback { +public class OsiMockServerExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback { private WireMockServer serviceKontoMockServer; private WireMockServer postfachFacadeMockServer; @@ -30,26 +34,36 @@ public class OsiMockServerExtension implements BeforeAllCallback, AfterAllCallba setupServiceKontoMock(); } - @Override - public void afterEach(ExtensionContext context) { - postfachFacadeMockServer.resetAll(); - serviceKontoMockServer.resetAll(); - } - @Override public void afterAll(ExtensionContext context) { - serviceKontoMockServer.shutdown(); - postfachFacadeMockServer.shutdown(); + if (serviceKontoMockServer != null) { + serviceKontoMockServer.shutdown(); + serviceKontoMockServer = null; + } + + if (postfachFacadeMockServer != null) { + postfachFacadeMockServer.shutdown(); + postfachFacadeMockServer = null; + } } private void setupPostfachFacadeMock() { - postfachFacadeMockServer = new WireMockServer(0); - postfachFacadeMockServer.start(); + postfachFacadeMockServer = setupWiremockServer(postfachFacadeMockServer, 32813); } private void setupServiceKontoMock() { - serviceKontoMockServer = new WireMockServer(0); - serviceKontoMockServer.start(); + serviceKontoMockServer = setupWiremockServer(serviceKontoMockServer, 32812); + } + + private WireMockServer setupWiremockServer(@Nullable WireMockServer existingServer, int port) { + if (existingServer != null && existingServer.isRunning()) { + existingServer.resetAll(); + return existingServer; + } else { + var server = new WireMockServer(port); + server.start(); + return server; + } } public String getAccessTokenUrl() { @@ -57,11 +71,16 @@ public class OsiMockServerExtension implements BeforeAllCallback, AfterAllCallba } public String getPostfachFacadeUrl() { - return postfachFacadeMockServer.baseUrl(); + return Optional.ofNullable(postfachFacadeMockServer) + .map(WireMockServer::baseUrl) + .orElseThrow(() -> new IllegalStateException("PostfachFacadeMockServer not initialized")); } @Override public void beforeEach(ExtensionContext context) { + setupPostfachFacadeMock(); + setupServiceKontoMock(); + serviceKontoMockServer.stubFor(post("/access-token") .withHeader("Content-Type", equalTo("application/x-www-form-urlencoded")) .withFormParam("grant_type", equalTo("client_credentials")) diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/VorgangManagerContainer.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/VorgangManagerContainer.java new file mode 100644 index 0000000000000000000000000000000000000000..a4d3ce6fa6c839f859d0102127988a414156633e --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/VorgangManagerContainer.java @@ -0,0 +1,30 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.extension; + +import java.time.Duration; + +import jakarta.validation.constraints.NotNull; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; + +public class VorgangManagerContainer extends GenericContainer<VorgangManagerContainer> { + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("docker.ozg-sh.de/vorgang-manager"); + private static final String DEFAULT_TAG = "latest"; + public static final int PORT = 9090; + + public VorgangManagerContainer() { + this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG)); + } + + public VorgangManagerContainer(@NotNull DockerImageName dockerImageName) { + super(dockerImageName); + addExposedPort(PORT); + super.withStartupTimeout(Duration.ofMinutes(2)); + } + + public String getAddress() { + return "%s:%d".formatted(getHost(), getMappedPort(PORT)); + } + +} diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/VorgangManagerServerExtension.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/VorgangManagerServerExtension.java new file mode 100644 index 0000000000000000000000000000000000000000..6de18dcd892e9a35984e2d46dee5368be30a341e --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/VorgangManagerServerExtension.java @@ -0,0 +1,83 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.extension; + +import java.time.Duration; +import java.util.Map; +import java.util.Optional; + +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.OutputFrame; +import org.testcontainers.lifecycle.Startables; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@Getter +@RequiredArgsConstructor +public class VorgangManagerServerExtension implements BeforeAllCallback, AfterAllCallback { + + static final String VORGANG_MANAGER_NETWORK_ALIAS = "vorgang-manager"; + static final String MONGODB_NETWORK_ALIAS = "mongodb"; + + private VorgangManagerContainer vorgangManagerContainer; + private MongoDBContainer mongoDBContainer; + + @Override + public void beforeAll(ExtensionContext context) { + if (vorgangManagerContainer == null || mongoDBContainer == null) { + setupVorgangManager(); + } + } + + @SneakyThrows + private void setupVorgangManager() { + var network = Network.newNetwork(); + mongoDBContainer = new MongoDBContainer("mongo:7.0") + .withStartupTimeout(Duration.ofMinutes(1)) + .withNetworkAliases(MONGODB_NETWORK_ALIAS) + .withNetwork(network); + vorgangManagerContainer = new VorgangManagerContainer() + .withEnv(Map.of( + "spring_profiles_active", "local", + "ozgcloud.vorgang-manager.serviceAddress", "%s:%d".formatted(VORGANG_MANAGER_NETWORK_ALIAS, VorgangManagerContainer.PORT), + "spring.data.mongodb.host", MONGODB_NETWORK_ALIAS, + "spring.data.mongodb.port", "27017", + "mongock_enabled", "false" + )) + .withNetwork(network) + .withLogConsumer(this::logVorgangManager) + .withStartupTimeout(Duration.ofMinutes(2)) + .dependsOn(mongoDBContainer); + Startables.deepStart(vorgangManagerContainer, mongoDBContainer).get(); + } + + private void logVorgangManager(OutputFrame outputFrame) { + var stream = outputFrame.getType() == OutputFrame.OutputType.STDOUT ? System.out : System.err; + stream.println("[vorgang-manager] " + outputFrame.getUtf8String().stripTrailing()); + } + + @Override + public void afterAll(ExtensionContext context) { + if (mongoDBContainer != null) { + mongoDBContainer.stop(); + mongoDBContainer = null; + } + if (vorgangManagerContainer != null) { + vorgangManagerContainer.stop(); + vorgangManagerContainer = null; + } + } + + public String getVorgangManagerAddress() { + return Optional.ofNullable(vorgangManagerContainer) + .map(VorgangManagerContainer::getAddress) + .orElse("missing"); + } + +} diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/AttachmentFileTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/AttachmentFileTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..5d8056e79ea008aac5285a8eb1a01ca9bdcdba6c --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/AttachmentFileTestFactory.java @@ -0,0 +1,20 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.factory; + +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2FileUploadTestFactory.*; +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory.*; + +import de.ozgcloud.nachrichten.postfach.osiv2.model.AttachmentFile; + +public class AttachmentFileTestFactory { + + public static AttachmentFile create() { + return createBuilder().build(); + } + + public static AttachmentFile.AttachmentFileBuilder createBuilder() { + return AttachmentFile.builder() + .name(UPLOAD_NAME) + .contentType(UPLOAD_CONTENT_TYPE) + .vorgangId(VORGANG_ID); + } +} diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/AttachmentInfoTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/AttachmentInfoTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..8758edfbcd085c225876fa9c8e4ff34dcdbaee74 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/AttachmentInfoTestFactory.java @@ -0,0 +1,20 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.factory; + +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2FileUploadTestFactory.*; + +import de.ozgcloud.nachrichten.postfach.osiv2.model.AttachmentInfo; + +public class AttachmentInfoTestFactory { + + public static AttachmentInfo create() { + return createBuilder().build(); + } + + public static AttachmentInfo.AttachmentInfoBuilder createBuilder() { + return AttachmentInfo.builder() + .name(UPLOAD_NAME) + .contentType(UPLOAD_CONTENT_TYPE) + .guid(UPLOAD_GUID) + .size(UPLOAD_SIZE); + } +} diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/DummyStringBasedIdentifier.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/DummyStringBasedIdentifier.java deleted file mode 100644 index cc2ef17e289807775b21c42b498a969faba0d1a5..0000000000000000000000000000000000000000 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/DummyStringBasedIdentifier.java +++ /dev/null @@ -1,23 +0,0 @@ -package de.ozgcloud.nachrichten.postfach.osiv2.factory; - -import de.ozgcloud.nachrichten.postfach.PostfachAddressIdentifier; -import lombok.Builder; -import lombok.Getter; - -@Builder -@Getter -public class DummyStringBasedIdentifier implements PostfachAddressIdentifier { - - private String mailboxId; - - @Override - public boolean isStringBasedIdentifier() { - return true; - } - - @Override - public String toString() { - return mailboxId; - } -} - diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/FileChunkInfoTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/FileChunkInfoTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..f720d9bf668a6cff0a7401c249142d7e23d10aac --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/FileChunkInfoTestFactory.java @@ -0,0 +1,18 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.factory; + +import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo; + +public class FileChunkInfoTestFactory { + + public static final Integer CHUNK_INDEX = 1; + + public static FileChunkInfo create() { + return createBuilder().build(); + } + + public static FileChunkInfo.FileChunkInfoBuilder createBuilder() { + return FileChunkInfo.builder() + .upload(Osi2FileUploadTestFactory.create()) + .chunkIndex(CHUNK_INDEX); + } +} diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/GrpcOzgFileTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/GrpcOzgFileTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..c72aa9d89db1e52e5fb319a7bc5a8ea7211f51c2 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/GrpcOzgFileTestFactory.java @@ -0,0 +1,24 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.factory; + +import de.ozgcloud.vorgang.grpc.file.GrpcOzgFile; + +public class GrpcOzgFileTestFactory { + + public static final String FILE_ID = "ozgfileId1"; + public static final String FILE_ID_2 = "ozgfileId2"; + public static final String FILE_NAME = "ozgfileName1"; + public static final String FILE_CONTENT_TYPE = "ozgfileContentType1"; + public static final Long FILE_SIZE = 124L; + + public static GrpcOzgFile create() { + return createBuilder().build(); + } + + public static GrpcOzgFile.Builder createBuilder() { + return GrpcOzgFile.newBuilder() + .setId(FILE_ID) + .setName(FILE_NAME) + .setContentType(FILE_CONTENT_TYPE) + .setSize(FILE_SIZE); + } +} diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeReceiveMessageTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeReceiveMessageTestFactory.java index 75a60a29f366e9ea29f68686f234a02c0d8fd987..278b052eec80c315137367d08c0af3d2f74e244e 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeReceiveMessageTestFactory.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeReceiveMessageTestFactory.java @@ -1,15 +1,25 @@ package de.ozgcloud.nachrichten.postfach.osiv2.factory; +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.V1ReplyMessageTestFactory.*; + +import java.util.Arrays; import java.util.UUID; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeReceiveAttachment; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeReceiveMessage; public class MessageExchangeReceiveMessageTestFactory { - public static final UUID MESSAGE_ID = UUID.randomUUID(); + public static final String ATTACHMENT_1_ID = UUID.randomUUID().toString(); + public static final String ATTACHMENT_2_ID = UUID.randomUUID().toString(); - public static MessageExchangeReceiveMessage create() { + public static MessageExchangeReceiveMessage create(String... attachmentIds) { return new MessageExchangeReceiveMessage() - .guid(MESSAGE_ID); + .guid(UUID.fromString(MESSAGE_ID)) + .attachments(attachmentIds.length == 0 ? null : Arrays.stream(attachmentIds) + .map(attachmentId -> MessageExchangeReceiveAttachment.builder() + .guid(UUID.fromString(attachmentId)) + .build()) + .toList()); } } diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeReceiveMessagesResponseTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeReceiveMessagesResponseTestFactory.java index a104cfc9d8ee0dc6d185b871a27103b7f9d55542..70525c804aa0c2d262f54afafcde661b6ebb4bc6 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeReceiveMessagesResponseTestFactory.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeReceiveMessagesResponseTestFactory.java @@ -14,7 +14,7 @@ public class MessageExchangeReceiveMessagesResponseTestFactory { return new MessageExchangeReceiveMessagesResponse() .messages(Arrays.stream(uuids) .map(uuid -> MessageExchangeReceiveMessageTestFactory.create() - .guid(UUID.fromString(uuid))) + .guid(uuid == null ? null : UUID.fromString(uuid))) .toList()); } diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeSendMessageResponseTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeSendMessageResponseTestFactory.java index 0c296a6f816a801dcf5a7f2b4ba6e6e3b8e1d653..d0c543cd29af2a128b293579450fcf7b5dd3663e 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeSendMessageResponseTestFactory.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/MessageExchangeSendMessageResponseTestFactory.java @@ -1,13 +1,19 @@ package de.ozgcloud.nachrichten.postfach.osiv2.factory; -import static de.ozgcloud.nachrichten.postfach.osiv2.factory.MessageExchangeReceiveMessageTestFactory.*; +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.V1ReplyMessageTestFactory.*; + +import java.util.UUID; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeSendMessageResponse; public class MessageExchangeSendMessageResponseTestFactory { public static MessageExchangeSendMessageResponse create() { - return new MessageExchangeSendMessageResponse() - .messageId(MESSAGE_ID); + return createBuilder().build(); + } + + public static MessageExchangeSendMessageResponse.Builder createBuilder() { + return MessageExchangeSendMessageResponse.builder() + .messageId(UUID.fromString(MESSAGE_ID)); } } diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/Osi2FileUploadTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/Osi2FileUploadTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..a7e615363f7c878fbdba76251584aeb514664e2d --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/Osi2FileUploadTestFactory.java @@ -0,0 +1,35 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.factory; + +import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment.*; + +import java.util.UUID; + +import de.ozgcloud.apilib.file.OzgCloudFile; +import de.ozgcloud.apilib.file.OzgCloudFileId; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment; + +public class Osi2FileUploadTestFactory { + + public static final String UPLOAD_FILE_ID = UUID.randomUUID().toString(); + public static final String UPLOAD_GUID = UUID.randomUUID().toString(); + public static final String UPLOAD_GUID2 = UUID.randomUUID().toString(); + public static final String UPLOAD_NAME = "upload.txt"; + public static final String UPLOAD_CONTENT_TYPE = "text/plain"; + public static final long NUMBER_OF_CHUNKS = 5; + public static final long UPLOAD_SIZE = CHUNK_SIZE * NUMBER_OF_CHUNKS; + + public static Osi2Attachment create() { + return createBuilder().build(); + } + + public static Osi2Attachment.Osi2AttachmentBuilder createBuilder() { + return Osi2Attachment.builder() + .guid(UPLOAD_GUID) + .file(OzgCloudFile.builder() + .id(new OzgCloudFileId(UPLOAD_FILE_ID)) + .name(UPLOAD_NAME) + .contentType(UPLOAD_CONTENT_TYPE) + .size(UPLOAD_SIZE) + .build()); + } +} diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/Osi2MessageTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/Osi2MessageTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..8d40f117e113c3c070288228e011c27c3ead4500 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/Osi2MessageTestFactory.java @@ -0,0 +1,29 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.factory; + +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory.*; +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.V1ReplyMessageTestFactory.*; + +import java.time.ZonedDateTime; +import java.util.List; + +import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message; + +public class Osi2MessageTestFactory { + + public static Osi2Message create() { + return createBuilder().build(); + } + + public static Osi2Message.Osi2MessageBuilder createBuilder() { + return Osi2Message.builder() + .messageId(MESSAGE_ID) + .mailBody(MAIL_BODY) + .subject(MAIL_SUBJECT) + .replyOption(PostfachNachricht.ReplyOption.FORBIDDEN) + .createdAt(ZonedDateTime.now()) + .vorgangId(VORGANG_ID) + .attachments(List.of()) + .postfachAddress(PostfachAddressTestFactory.create()); + } +} diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/PostfachAddressTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/PostfachAddressTestFactory.java index 046f298c7060ef696550ed252f7b51c097e297fe..de986e02f39c9781e3af9be92741d4d0b64994de 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/PostfachAddressTestFactory.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/PostfachAddressTestFactory.java @@ -1,10 +1,13 @@ package de.ozgcloud.nachrichten.postfach.osiv2.factory; +import java.util.UUID; + import de.ozgcloud.nachrichten.postfach.PostfachAddress; +import de.ozgcloud.nachrichten.postfach.StringBasedIdentifier; public class PostfachAddressTestFactory { - public static final String MAILBOX_ID = "testMailboxId"; + public static final String MAILBOX_ID = UUID.randomUUID().toString(); public static final String SERVICE_KONTO_TYPE = "TYPE1"; public static PostfachAddress create() { @@ -14,9 +17,9 @@ public class PostfachAddressTestFactory { public static PostfachAddress.PostfachAddressBuilder createBuilder() { return PostfachAddress.builder() .type(1) - .serviceKontoType("TYPE1") - .identifier(DummyStringBasedIdentifier.builder() - .mailboxId(MAILBOX_ID) + .serviceKontoType(SERVICE_KONTO_TYPE) + .identifier(StringBasedIdentifier.builder() + .postfachId(MAILBOX_ID) .build()); } } diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/QuarantineFileResultTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/QuarantineFileResultTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..c50c8233f6224e68f54c3c50e68a52f0bcef05d4 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/QuarantineFileResultTestFactory.java @@ -0,0 +1,19 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.factory; + +import com.thedeanda.lorem.LoremIpsum; + +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.QuarantineFileResult; + +public class QuarantineFileResultTestFactory { + + public static final String ERROR_STRING = LoremIpsum.getInstance().getWords(10); + + public static QuarantineFileResult create() { + return createBuilder().build(); + } + + public static QuarantineFileResult.Builder createBuilder() { + return QuarantineFileResult.builder() + .success(true); + } +} diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/V1ReplyFilesTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/V1ReplyFilesTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..1974823886b4b815b84756089b97012c9ecf31de --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/V1ReplyFilesTestFactory.java @@ -0,0 +1,25 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.factory; + +import java.util.UUID; + +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyFiles; + +public class V1ReplyFilesTestFactory { + + public static final String ATTACHMENT_GUID = UUID.randomUUID().toString(); + public static final String ATTACHMENT_NAME = "test.txt"; + public static final String ATTACHMENT_CONTENT_TYPE = "text/plain"; + public static final Long ATTACHMENT_SIZE = 1000L; + + public static V1ReplyFiles create() { + return createBuilder().build(); + } + + public static V1ReplyFiles.Builder createBuilder() { + return V1ReplyFiles.builder() + .guid(UUID.fromString(ATTACHMENT_GUID)) + .name(ATTACHMENT_NAME) + .mimeType(ATTACHMENT_CONTENT_TYPE) + .size(ATTACHMENT_SIZE); + } +} diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/V1ReplyMessageTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/V1ReplyMessageTestFactory.java index 2bb4c78fa25a9d2a80f4b249c9b0e2b57b8dea74..eb41e7858d336007b5e8ad8814084aab06a60323 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/V1ReplyMessageTestFactory.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/V1ReplyMessageTestFactory.java @@ -2,6 +2,7 @@ package de.ozgcloud.nachrichten.postfach.osiv2.factory; import java.time.OffsetDateTime; import java.time.ZonedDateTime; +import java.util.List; import java.util.UUID; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1EidasLevel; @@ -23,14 +24,20 @@ public class V1ReplyMessageTestFactory { public static final ZonedDateTime RESPONSE_TIME = ZonedDateTime.now(); public static V1ReplyMessage create() { - return new V1ReplyMessage() + return createBuilder().build(); + } + + public static V1ReplyMessage.Builder createBuilder() { + return V1ReplyMessage.builder() .sequencenumber(SEQUENCE_NUMMER) .subject(SUBJECT) .replyAction(V1ReplyBehavior.REPLYPOSSIBLE) .isObligatory(false) .eidasLevel(V1EidasLevel.LOW) .isHtml(false) + .body(REPLY_BODY) .guid(UUID.fromString(MESSAGE_ID)) + .files(List.of(V1ReplyFilesTestFactory.create())) .messageBox(UUID.fromString(MESSAGE_BOX_ID)) .responseTime(OffsetDateTime.of(RESPONSE_TIME.toLocalDateTime(), RESPONSE_TIME.getOffset())); } diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2AttachmentTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2AttachmentTest.java new file mode 100644 index 0000000000000000000000000000000000000000..bbe1e1f9f47bc69a33bfbfc23206452c0165552c --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2AttachmentTest.java @@ -0,0 +1,60 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.model; + +import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment.*; +import static org.assertj.core.api.Assertions.*; + +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 de.ozgcloud.apilib.file.OzgCloudFileTestFactory; + +public class Osi2AttachmentTest { + + @DisplayName("calculate number of chunks") + @Nested + class TestCalculateNumberOfChunks { + + @DisplayName("should return zero") + @Test + void shouldReturnZero() { + var upload = createWithSize(0L); + + var result = upload.numberOfChunks(); + + assertThat(result).isZero(); + } + + @DisplayName("should return one") + @ParameterizedTest + @ValueSource(longs = { 1, CHUNK_SIZE - 1, CHUNK_SIZE }) + void shouldReturnOne(long fileLength) { + var upload = createWithSize(fileLength); + + var result = upload.numberOfChunks(); + + assertThat(result).isEqualTo(1L); + } + + @DisplayName("should return two") + @ParameterizedTest + @ValueSource(longs = { CHUNK_SIZE + 1, CHUNK_SIZE * 2 - 1, CHUNK_SIZE * 2 }) + void shouldReturnTwo(long fileLength) { + var upload = createWithSize(fileLength); + + var result = upload.numberOfChunks(); + + assertThat(result).isEqualTo(2L); + } + + private Osi2Attachment createWithSize(long size) { + return Osi2Attachment.builder() + .file(OzgCloudFileTestFactory.createBuilder() + .size(size) + .build()) + .build(); + } + } +} diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2MessageMapperTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2MessageMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..89d64bac79517999fc840fd5c2857126c879fa51 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2MessageMapperTest.java @@ -0,0 +1,98 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.transfer; + +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory.*; +import static java.util.Collections.*; +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mapstruct.factory.Mappers; + +import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2MessageTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message; + +class Osi2MessageMapperTest { + + private final Osi2MessageMapper mapper = Mappers.getMapper(Osi2MessageMapper.class); + + @DisplayName("to PostfachNachricht") + @Nested + class TestToPostfachNachricht { + private final Osi2Message osi2Message = Osi2MessageTestFactory.create(); + private final String attachmentId1 = "attachmentId1"; + private final String attachmentId2 = "attachmentId2"; + + @DisplayName("should map vorgang id") + @Test + void shouldMapVorgangId() { + var result = doMapping(); + + assertThat(result.getVorgangId()).isEqualTo(VORGANG_ID); + } + + @DisplayName("should map mail body") + @Test + void shouldMapMailBody() { + var result = doMapping(); + + assertThat(result.getMailBody()).isEqualTo(MAIL_BODY); + } + + @DisplayName("should map subject") + @Test + void shouldMapSubject() { + var result = doMapping(); + + assertThat(result.getSubject()).isEqualTo(MAIL_SUBJECT); + } + + @DisplayName("should map reply option") + @Test + void shouldMapReplyOption() { + var result = doMapping(); + + assertThat(result.getReplyOption()).isEqualTo(PostfachNachricht.ReplyOption.FORBIDDEN); + } + + @DisplayName("should map created at") + @Test + void shouldMapCreatedAt() { + var result = doMapping(); + + assertThat(result.getCreatedAt()).isEqualTo(osi2Message.createdAt()); + } + + @DisplayName("should map postfach address") + @Test + void shouldMapPostfachAddress() { + var result = doMapping(); + + assertThat(result.getPostfachAddress()).isEqualTo(osi2Message.postfachAddress()); + } + + @DisplayName("should map direction") + @Test + void shouldMapDirection() { + var result = doMapping(); + + assertThat(result.getDirection()).isEqualTo(PostfachNachricht.Direction.IN); + } + + @DisplayName("should map attachments") + @Test + void shouldMapAttachments() { + var result = doMapping(); + + assertThat(result.getAttachments()).containsExactly(attachmentId1, attachmentId2); + } + + private PostfachNachricht doMapping() { + return mapper.toPostfachNachricht(osi2Message, List.of(attachmentId1, attachmentId2)); + } + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7077d02f8f7ad83c5dd3171056079beac450b06e --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachServiceTest.java @@ -0,0 +1,96 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.transfer; + +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.MessageExchangeReceiveMessagesResponseTestFactory.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.stream.Stream; + +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.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2FileUploadTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment; + +class Osi2PostfachServiceTest { + + @Spy + @InjectMocks + private Osi2PostfachService service; + + @Mock + private PostfachApiFacadeService postfachApiFacadeService; + @Mock + private Osi2QuarantineService quarantineService; + + @DisplayName("send message") + @Nested + class TestSendOsi2Message { + private final PostfachNachricht nachricht = PostfachNachrichtTestFactory.create(); + private final List<Osi2Attachment> uploadFiles = List.of(Osi2FileUploadTestFactory.create()); + + @BeforeEach + void mock() { + when(quarantineService.uploadAttachments(any())).thenReturn(uploadFiles); + } + + @DisplayName("should send message") + @Test + void shouldSendMessage() { + service.sendMessage(nachricht); + + verify(postfachApiFacadeService).sendMessage(nachricht, uploadFiles); + } + + @DisplayName("should upload attachments of nachricht") + @Test + void shouldUploadAttachmentsOfNachricht() { + service.sendMessage(nachricht); + + verify(quarantineService).uploadAttachments(nachricht.getAttachments()); + } + } + + @DisplayName("receive messages") + @Nested + class TestReceiveOsi2Message { + + @DisplayName("with two pending messages") + @Nested + class TestWithTwoPendingMessages { + + @BeforeEach + void mock() { + when(postfachApiFacadeService.fetchPendingMessageIds()).thenReturn(List.of(MESSAGE_ID_1, MESSAGE_ID_2)); + doReturn(PostfachNachrichtTestFactory.createBuilder().messageId(MESSAGE_ID_1).build()) + .when(service).fetchPostfachNachricht(MESSAGE_ID_1); + doReturn(PostfachNachrichtTestFactory.createBuilder().messageId(MESSAGE_ID_2).build()) + .when(service).fetchPostfachNachricht(MESSAGE_ID_2); + } + + @DisplayName("should return") + @Test + void shouldReturn() { + var messages = receiveMessages(); + + assertThat(messages) + .extracting(PostfachNachricht::getMessageId) + .containsExactly(MESSAGE_ID_1, MESSAGE_ID_2); + } + + } + + private Stream<PostfachNachricht> receiveMessages() { + return service.receiveMessages(); + } + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000000000000000000000000000000000000..c411ff93efc7e6beb5cfe416f00fc2128b36fdf7 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineServiceTest.java @@ -0,0 +1,466 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.transfer; + +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2FileUploadTestFactory.*; +import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment.*; +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.attachment.Osi2AttachmentFileService; +import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2RuntimeException; +import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2UploadException; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2FileUploadTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.QuarantineStatus; +import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment; +import lombok.SneakyThrows; + +class Osi2QuarantineServiceTest { + + @InjectMocks + @Spy + private Osi2QuarantineService service; + + @Mock + private PostfachApiFacadeService postfachApiFacadeService; + + @Mock + private Osi2AttachmentFileService binaryFileService; + + private final OzgCloudFile file1 = OzgCloudFileTestFactory.createBuilder() + .id(new OzgCloudFileId(UPLOAD_FILE_ID)) + .size(1000L) + .build(); + private final OzgCloudFile file2 = OzgCloudFileTestFactory.createBuilder() + .size(2000L) + .build(); + + private final Osi2Attachment upload1 = Osi2FileUploadTestFactory.createBuilder() + .file(file1) + .build(); + private final Osi2Attachment upload2 = Osi2FileUploadTestFactory.createBuilder() + .guid(UUID.randomUUID().toString()) + .file(file2) + .build(); + private final Osi2Attachment upload3 = Osi2FileUploadTestFactory.createBuilder() + .guid(UUID.randomUUID().toString()) + .build(); + + private final List<Osi2Attachment> uploads = List.of(upload1, upload2, upload3); + + @DisplayName("upload attachments") + @Nested + class TestUploadAttachments { + + @BeforeEach + void mock() { + when(binaryFileService.getFileMetadataOfIds(any())).thenReturn(List.of(file1)); + doReturn(List.of(upload1)).when(service).uploadFiles(any()); + } + + @DisplayName("should call getFileMetadataOfIds") + @Test + void shouldCallGetFileMetadataOfIds() { + uploadAttachments(); + + verify(binaryFileService).getFileMetadataOfIds(List.of(UPLOAD_FILE_ID)); + } + + @DisplayName("should call uploadFiles") + @Test + void shouldCallUploadFilesToQuarantineWithLargestFileFirst() { + uploadAttachments(); + + verify(service).uploadFiles(List.of(file1)); + } + + @DisplayName("should return") + @Test + void shouldReturn() { + var result = uploadAttachments(); + + assertThat(result).containsExactly(upload1); + } + + List<Osi2Attachment> uploadAttachments() { + return service.uploadAttachments(List.of(UPLOAD_FILE_ID)); + } + } + + @DisplayName("upload files") + @Nested + class TestUploadFiles { + + @BeforeEach + void mock() { + doReturn(uploads).when(service).deriveSortedUploadFiles(any()); + } + + @DisplayName("without exception") + @Nested + class TestWithoutException { + @BeforeEach + void mock() { + doNothing().when(service).tryUploadSortedFiles(any()); + } + + @DisplayName("should call deriveSortedUploadFiles") + @Test + void shouldCallDeriveSortedUploadFiles() { + uploadFiles(); + + verify(service).deriveSortedUploadFiles(List.of(file1)); + } + + @DisplayName("should call uploadFiles") + @Test + void shouldCallUploadFilesToQuarantineWithLargestFileFirst() { + uploadFiles(); + + verify(service).tryUploadSortedFiles(uploads); + } + + @DisplayName("should return") + @Test + void shouldReturn() { + var result = uploadFiles(); + + assertThat(result).containsExactly(upload1, upload2, upload3); + } + } + + @DisplayName("with exception") + @Nested + class TestWithException { + + private final RuntimeException exception = new RuntimeException("test"); + + @BeforeEach + void mock() { + doThrow(exception).when(service).tryUploadSortedFiles(any()); + } + + @DisplayName("should delete attachments") + @Test + void shouldDeleteAttachments() { + try { + uploadFiles(); + } catch (RuntimeException e) { + // ignore + } + + verify(service).deleteAttachments(uploads); + } + + @DisplayName("should rethrow exception") + @Test + void shouldRethrowException() { + assertThatThrownBy(TestUploadFiles.this::uploadFiles) + .isEqualTo(exception); + } + } + + List<Osi2Attachment> uploadFiles() { + return service.uploadFiles(List.of(file1)); + } + } + + @DisplayName("derive sorted upload files") + @Nested + class TestDeriveSortedUploadFiles { + + @DisplayName("should return largest file first") + @Test + void shouldReturnLargestFileFirst() { + var result = service.deriveSortedUploadFiles(List.of(file1, file2)); + + assertThat(result) + .extracting(Osi2Attachment::file) + .containsExactly(file2, file1); + } + + @DisplayName("should have random upload messageGuid") + @Test + void shouldHaveRandomUploadGuid() { + var result = service.deriveSortedUploadFiles(List.of(file1, file2)); + + assertThat(result) + .extracting(Osi2Attachment::guid) + .extracting(String::length) + .containsExactly(36, 36); + } + } + + @DisplayName("try upload sorted files") + @Nested + class TestTryUploadSortedFiles { + + @BeforeEach + void mock() { + doNothing().when(service).uploadFilesToQuarantine(any()); + doNothing().when(service).waitForVirusScan(any()); + } + + @DisplayName("should call uploadFilesToQuarantine") + @Test + void shouldCallUploadFilesToQuarantine() { + tryUploadSortedFiles(); + + verify(service).uploadFilesToQuarantine(uploads); + } + + @DisplayName("should call waitForVirusScan") + @Test + void shouldCallWaitForVirusScan() { + tryUploadSortedFiles(); + + verify(service).waitForVirusScan(uploads); + } + + void tryUploadSortedFiles() { + service.tryUploadSortedFiles(uploads); + } + } + + @DisplayName("upload files to quarantine") + @Nested + class TestUploadFilesToQuarantine { + + @DisplayName("should call uploadFileToQuarantine for each file") + @Test + void shouldCallUploadFileToQuarantineForEachFile() { + doNothing().when(service).uploadFileToQuarantine(any()); + + service.uploadFilesToQuarantine(uploads); + + verify(service).uploadFileToQuarantine(upload1); + verify(service).uploadFileToQuarantine(upload2); + verify(service).uploadFileToQuarantine(upload3); + } + } + + @DisplayName("upload file to quarantine") + @Nested + class TestUploadFileToQuarantine { + + @Mock + private InputStream fileInputStream; + + @BeforeEach + void mock() { + when(binaryFileService.downloadFileContent(any())).thenReturn(fileInputStream); + doNothing().when(service).uploadInputStreamToQuarantine(any(), any()); + } + + @DisplayName("should call streamFileContent") + @Test + void shouldCallStreamFileContent() { + uploadFileToQuarantine(); + + verify(binaryFileService).downloadFileContent(UPLOAD_FILE_ID); + } + + @DisplayName("should call uploadInputStreamToQuarantine") + @Test + void shouldCallUploadInputStreamToQuarantine() { + uploadFileToQuarantine(); + + verify(service).uploadInputStreamToQuarantine(upload1, fileInputStream); + } + + @DisplayName("should close inputStream") + @Test + @SneakyThrows + void shouldCloseInputStream() { + uploadFileToQuarantine(); + + verify(fileInputStream).close(); + } + + @DisplayName("should throw OsiPostfachException if close fails with IOException") + @Test + @SneakyThrows + void shouldThrowOsiPostfachException() { + doThrow(new IOException("test")).when(fileInputStream).close(); + + assertThatThrownBy(this::uploadFileToQuarantine) + .isInstanceOf(Osi2RuntimeException.class); + } + + void uploadFileToQuarantine() { + service.uploadFileToQuarantine(upload1); + } + } + + @DisplayName("stream file chunk infos") + @Nested + class TestStreamFileChunkInfos { + + private final Osi2Attachment upload = Osi2FileUploadTestFactory.createBuilder() + .file(OzgCloudFileTestFactory.createBuilder() + .size(CHUNK_SIZE * 2) + .build()) + .build(); + + private final FileChunkInfo chunkInfo1 = FileChunkInfo.builder() + .upload(upload) + .chunkIndex(0) + .build(); + private final FileChunkInfo chunkInfo2 = FileChunkInfo.builder() + .upload(upload) + .chunkIndex(1) + .build(); + private final FileChunkInfo emptyChunkInfo = FileChunkInfo.builder() + .upload(upload) + .chunkIndex(2) + .build(); + + @DisplayName("should stream chunk infos") + @Test + void shouldStreamChunkInfos() { + var result = service.streamFileChunkInfos(upload).toList(); + + assertThat(result).containsExactly(chunkInfo1, chunkInfo2, emptyChunkInfo); + } + } + + @DisplayName("check virus scan completed") + @Nested + class TestCheckVirusScanCompleted { + + @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 Osi2Attachment 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 { + + @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); + } + } + } + + @DisplayName("delete attachments") + @Nested + class TestDeleteAttachments { + @DisplayName("should call deleteAttachment for uploads") + @Test + void shouldCallDeleteAttachmentForUploads() { + service.deleteAttachments(uploads); + + verify(postfachApiFacadeService).deleteFileUpload(upload1.guid()); + verify(postfachApiFacadeService).deleteFileUpload(upload2.guid()); + verify(postfachApiFacadeService).deleteFileUpload(upload3.guid()); + } + } + +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapperTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapperTest.java index 2f1cbe4af9940226535af88324de563b10261338..bdf27c7d4d9171eb1c1b60da3ede0638510b176b 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapperTest.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapperTest.java @@ -1,9 +1,13 @@ package de.ozgcloud.nachrichten.postfach.osiv2.transfer; +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2FileUploadTestFactory.*; import static de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachAddressTestFactory.*; import static de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory.*; +import static java.util.Collections.*; import static org.assertj.core.api.Assertions.*; +import java.util.List; +import java.util.UUID; import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; @@ -14,14 +18,20 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.mapstruct.factory.Mappers; +import de.ozgcloud.apilib.file.OzgCloudFileTestFactory; import de.ozgcloud.nachrichten.postfach.PostfachAddress; -import de.ozgcloud.nachrichten.postfach.PostfachAddressIdentifier; import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.FileChunkInfoTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2FileUploadTestFactory; import de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachAddressTestFactory; import de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.DomainChunkMetaData; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeFiles; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.OutSendMessageRequestV2; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1EidasLevel; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1FilestorageTarget; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyBehavior; +import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo; class Osi2RequestMapperTest { @@ -56,11 +66,6 @@ class Osi2RequestMapperTest { null, Arguments.of(PostfachAddressTestFactory.createBuilder() .identifier(null) - .build()), - Arguments.of(PostfachAddressTestFactory.createBuilder() - .identifier(new PostfachAddressIdentifier() { - - }) .build()) ); } @@ -68,7 +73,7 @@ class Osi2RequestMapperTest { @DisplayName("map OutSendMessageRequestV2") @Nested - class TestMapOutSendMessageRequestV2 { + class TestMapOutSendOsi2MessageRequestV2 { @DisplayName("should map sequence number") @Test @@ -117,7 +122,7 @@ class Osi2RequestMapperTest { .replyOption(PostfachNachricht.ReplyOption.POSSIBLE) .build(); - var result = mapper.mapOutSendMessageRequestV2(nachricht); + var result = mapper.mapOutSendMessageRequestV2(nachricht, emptyList()); assertThat(result.getReplyAction()).isEqualTo(V1ReplyBehavior.REPLYPOSSIBLE); } @@ -129,7 +134,7 @@ class Osi2RequestMapperTest { .replyOption(PostfachNachricht.ReplyOption.MANDATORY) .build(); - var result = mapper.mapOutSendMessageRequestV2(nachricht); + var result = mapper.mapOutSendMessageRequestV2(nachricht, emptyList()); assertThat(result.getReplyAction()).isEqualTo(V1ReplyBehavior.REPLYMANDATORY); } @@ -158,10 +163,42 @@ class Osi2RequestMapperTest { assertThat(result.getIsHtml()).isFalse(); } + @DisplayName("should map files if empty") + @Test + void shouldMapFilesIfEmpty() { + var result = doMapping(); + + assertThat(result.getFiles()).isEmpty(); + } + + @DisplayName("should map two files") + @Test + void shouldMapTwoFiles() { + var files = List.of( + Osi2FileUploadTestFactory.create(), + Osi2FileUploadTestFactory.createBuilder() + .file(OzgCloudFileTestFactory.create()) + .guid(UPLOAD_GUID2) + .build() + ); + var nachricht = PostfachNachrichtTestFactory.createBuilder() + .attachments(List.of(UPLOAD_FILE_ID, OzgCloudFileTestFactory.ID_STR)) + .build(); + + var result = mapper.mapOutSendMessageRequestV2(nachricht, files); + + assertThat(result.getFiles()) + .usingRecursiveComparison() + .isEqualTo(files.stream().map(mapper::mapMessageExchangeFile).toList()); + } + @DisplayName("should map files") @Test void shouldMapFiles() { - var result = doMapping(); + var result = mapper.mapOutSendMessageRequestV2( + PostfachNachrichtTestFactory.create(), + List.of(Osi2FileUploadTestFactory.create()) + ); assertThat(result.getFiles()).isEmpty(); } @@ -175,7 +212,114 @@ class Osi2RequestMapperTest { } private OutSendMessageRequestV2 doMapping() { - return mapper.mapOutSendMessageRequestV2(PostfachNachrichtTestFactory.create()); + return mapper.mapOutSendMessageRequestV2(PostfachNachrichtTestFactory.create(), emptyList()); + } + } + + @DisplayName("map message exchange file") + @Nested + class TestMapOsi2MessageExchangeFile { + @DisplayName("should map messageGuid") + @Test + void shouldMapGuid() { + var result = mapFile(); + + assertThat(result.getGuid()).isEqualTo(UUID.fromString(UPLOAD_GUID)); + } + + @DisplayName("should map mimeType") + @Test + void shouldMapMimeType() { + var result = mapFile(); + + assertThat(result.getMimeType()).isEqualTo(UPLOAD_CONTENT_TYPE); + } + + @DisplayName("should map name") + @Test + void shouldMapName() { + var result = mapFile(); + + assertThat(result.getName()).isEqualTo(UPLOAD_NAME); + } + + @DisplayName("should map size") + @Test + void shouldMapSize() { + var result = mapFile(); + + assertThat(result.getSize()).isEqualTo(UPLOAD_SIZE); + } + + private MessageExchangeFiles mapFile() { + return mapper.mapMessageExchangeFile(Osi2FileUploadTestFactory.create()); + } + } + + @DisplayName("map DomainChunkMetaData") + @Nested + class TestMapDomainChunkMetaData { + + private final FileChunkInfo chunkInfo = FileChunkInfoTestFactory.create(); + + @DisplayName("should map upload messageGuid") + @Test + void shouldMapUploadGuid() { + var result = doMapping(); + + assertThat(result.getUploadUid()).isEqualTo(UUID.fromString(UPLOAD_GUID)); + } + + @DisplayName("should map file name") + @Test + void shouldMapFileName() { + var result = doMapping(); + + assertThat(result.getFileName()).isEqualTo(UPLOAD_NAME); + } + + @DisplayName("should map content type") + @Test + void shouldMapContentType() { + var result = doMapping(); + + assertThat(result.getContentType()).isEqualTo(UPLOAD_CONTENT_TYPE); + } + + @DisplayName("should map chunk index") + @Test + void shouldMapChunkIndex() { + var result = doMapping(); + + assertThat(result.getChunkIndex()).isEqualTo(FileChunkInfoTestFactory.CHUNK_INDEX); + } + + @DisplayName("should map total chunks") + @Test + void shouldMapTotalChunks() { + var result = doMapping(); + + assertThat(result.getTotalChunks()).isEqualTo((int) NUMBER_OF_CHUNKS); + } + + @DisplayName("should map total file size") + @Test + void shouldMapTotalFileSize() { + var result = doMapping(); + + assertThat(result.getTotalFileSize()).isEqualTo(UPLOAD_SIZE); + } + + @DisplayName("should set target to unspecified") + @Test + void shouldSetTargetToUnspecified() { + var result = doMapping(); + + assertThat(result.getTarget()).isEqualTo(V1FilestorageTarget.UNSPECIFIED); + } + + private DomainChunkMetaData doMapping() { + return mapper.mapDomainChunkMetaData(chunkInfo); } } 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 8806cc719991a35cc7891a72b853542473eed084..ff5a336d4e16028c186505786504021ef711f814 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 @@ -1,5 +1,7 @@ package de.ozgcloud.nachrichten.postfach.osiv2.transfer; +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.QuarantineFileResultTestFactory.*; +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.V1ReplyFilesTestFactory.*; import static de.ozgcloud.nachrichten.postfach.osiv2.factory.V1ReplyMessageTestFactory.*; import static org.assertj.core.api.Assertions.*; @@ -16,9 +18,19 @@ import org.mapstruct.factory.Mappers; import org.mockito.InjectMocks; import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2RuntimeException; +import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2UploadException; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2FileUploadTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.QuarantineFileResultTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.V1ReplyFilesTestFactory; 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.V1ReplyFiles; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyMessage; +import de.ozgcloud.nachrichten.postfach.osiv2.model.AttachmentInfo; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message; +import lombok.SneakyThrows; class Osi2ResponseMapperTest { @@ -26,22 +38,22 @@ class Osi2ResponseMapperTest { private Osi2ResponseMapper mapper = Mappers.getMapper(Osi2ResponseMapper.class); private final V1ReplyMessage message = V1ReplyMessageTestFactory.create(); - @DisplayName("map V1ReplyMessage to PostfachNachricht") + @DisplayName("toMessage") @Nested - class V1ReplyMessageToPostfachNachricht { + class TestToMessage { @Test void shouldMapVorgangId() { var result = doMapping(); - assertThat(result.getVorgangId()).isEqualTo(SEQUENCE_NUMMER); + assertThat(result.vorgangId()).isEqualTo(SEQUENCE_NUMMER); } @Test void shouldMapPostfachAddress() { var result = doMapping(); - assertThat(result.getPostfachAddress().getIdentifier()) + assertThat(result.postfachAddress().getIdentifier()) .hasToString(MESSAGE_BOX_ID); } @@ -49,53 +61,52 @@ class Osi2ResponseMapperTest { void shouldMapCreatedAt() { var result = doMapping(); - assertThat(result.getCreatedAt()).isEqualTo(RESPONSE_TIME); - } - - @Test - void shouldMapDirection() { - var result = doMapping(); - - assertThat(result.getDirection()).isEqualTo(PostfachNachricht.Direction.IN); + assertThat(result.createdAt()).isEqualTo(RESPONSE_TIME); } @Test void shouldMapSubject() { var result = doMapping(); - assertThat(result.getSubject()).isEqualTo(SUBJECT); + assertThat(result.subject()).isEqualTo(SUBJECT); } @Test void shouldMapNullBodyToEmptyString() { - var result = doMapping(); + var noBodyMessage = V1ReplyMessageTestFactory.createBuilder() + .body(null) + .build(); + + var result = mapper.toMessage(noBodyMessage); - assertThat(result.getMailBody()).isEmpty(); + assertThat(result.mailBody()).isEmpty(); } @DisplayName("should map modified HTML body if HTML message") @Test void shouldMapModifiedHtmlBodyIfHtmlMessage() { - var htmlMessage = V1ReplyMessageTestFactory.create() + var htmlMessage = V1ReplyMessageTestFactory.createBuilder() .body(HTML_REPLY_BODY) - .isHtml(true); + .isHtml(true) + .build(); - var result = mapper.toPostfachNachricht(htmlMessage); + var result = mapper.toMessage(htmlMessage); - assertThat(result.getMailBody()).isEqualTo(REPLY_BODY); + assertThat(result.mailBody()).isEqualTo(REPLY_BODY); } @DisplayName("should map unmodified body if not HTML message") @ParameterizedTest @ValueSource(strings = { REPLY_BODY, HTML_REPLY_BODY }) void shouldMapUnmodifiedBodyIfNotHtmlMessage(String body) { - var htmlMessage = V1ReplyMessageTestFactory.create() + var htmlMessage = V1ReplyMessageTestFactory.createBuilder() .body(body) - .isHtml(false); + .isHtml(false) + .build(); - var result = mapper.toPostfachNachricht(htmlMessage); + var result = mapper.toMessage(htmlMessage); - assertThat(result.getMailBody()).isEqualTo(body); + assertThat(result.mailBody()).isEqualTo(body); } static Stream<Arguments> replyOptionValues() { @@ -110,13 +121,14 @@ class Osi2ResponseMapperTest { @ParameterizedTest @MethodSource("replyOptionValues") void shouldMapReplyOption(V1ReplyBehavior replyAction, PostfachNachricht.ReplyOption expected) { - var replyActionMessage = V1ReplyMessageTestFactory.create() + var replyActionMessage = V1ReplyMessageTestFactory.createBuilder() .replyAction(replyAction) - .isHtml(false); + .isHtml(false) + .build(); - var result = mapper.toPostfachNachricht(replyActionMessage); + var result = mapper.toMessage(replyActionMessage); - assertThat(result.getReplyOption()).isEqualTo(expected); + assertThat(result.replyOption()).isEqualTo(expected); } @DisplayName("should map messageId") @@ -124,7 +136,33 @@ class Osi2ResponseMapperTest { void shouldMapMessageId() { var result = doMapping(); - assertThat(result.getMessageId()).isEqualTo(MESSAGE_ID); + assertThat(result.messageId()).isEqualTo(MESSAGE_ID); + } + + @DisplayName("should map attachments") + @Test + void shouldMapAttachments() { + assert message.getFiles() != null; + var expectedAttachments = message.getFiles() + .stream() + .map(file -> mapper.toAttachmentInfo(file)) + .toList(); + + var result = doMapping(); + + assertThat(result.attachments()).usingRecursiveComparison().isEqualTo(expectedAttachments); + } + + @DisplayName("should map null attachments to empty list") + @Test + void shouldMapNullAttachmentsToEmptyList() { + var messageWithoutAttachments = V1ReplyMessageTestFactory.createBuilder() + .files(null) + .build(); + + var result = mapper.toMessage(messageWithoutAttachments); + + assertThat(result.attachments()).isEmpty(); } @DisplayName("should not fail if all fields are null") @@ -132,7 +170,7 @@ class Osi2ResponseMapperTest { void shouldNotFailIfAllFieldsAreNull() { var nullMessage = new V1ReplyMessage(); - assertThatCode(() -> mapper.toPostfachNachricht(nullMessage)) + assertThatCode(() -> mapper.toMessage(nullMessage)) .doesNotThrowAnyException(); } @@ -142,13 +180,119 @@ class Osi2ResponseMapperTest { var nullMessage = new V1ReplyMessage() .isHtml(true); - assertThatCode(() -> mapper.toPostfachNachricht(nullMessage)) + assertThatCode(() -> mapper.toMessage(nullMessage)) .doesNotThrowAnyException(); } - private PostfachNachricht doMapping() { - return mapper.toPostfachNachricht(message); + private Osi2Message doMapping() { + return mapper.toMessage(message); } + } + + @DisplayName("to AttachmentInfo") + @Nested + class TestToAttachmentInfo { + + private final V1ReplyFiles file = V1ReplyFilesTestFactory.create(); + @DisplayName("should map guid") + @Test + void shouldMapGuid() { + var result = doMapping(); + + assertThat(result.guid()).isEqualTo(ATTACHMENT_GUID); + } + + @DisplayName("should map name") + @Test + void shouldMapName() { + var result = doMapping(); + + assertThat(result.name()).isEqualTo(ATTACHMENT_NAME); + } + + @DisplayName("should map size") + @Test + void shouldMapSize() { + var result = doMapping(); + + assertThat(result.size()).isEqualTo(ATTACHMENT_SIZE); + } + + @DisplayName("should map content type") + @Test + void shouldMapContentType() { + var result = doMapping(); + + assertThat(result.contentType()).isEqualTo(ATTACHMENT_CONTENT_TYPE); + } + + private AttachmentInfo doMapping() { + return mapper.toAttachmentInfo(file); + } + } + + @DisplayName("is safe") + @Nested + class TestIsSafe { + @DisplayName("should throw for bad end status") + @ParameterizedTest + @ValueSource(strings = { "None", "Unsafe", "Corrupt", "Missing" }) + void shouldThrowForBadEndStatus(String status) { + var uploadStatus = QuarantineStatus.fromValue(status); + + assertThatThrownBy(() -> mapper.isSafe(uploadStatus)) + .isInstanceOf(Osi2UploadException.class) + .hasMessage(status); + } + + @DisplayName("should return false for unfinished status") + @ParameterizedTest + @ValueSource(strings = { "Legacy", "Commited" }) + @SneakyThrows + void shouldReturnFalseForUnfinishedStatus(String status) { + var uploadStatus = QuarantineStatus.fromValue(status); + + var result = mapper.isSafe(uploadStatus); + + assertThat(result).isFalse(); + } + + @DisplayName("should return true for safe status") + @Test + @SneakyThrows + void shouldReturnTrueForSafeStatus() { + var result = mapper.isSafe(QuarantineStatus.SAFE); + + assertThat(result).isTrue(); + } + } + + @DisplayName("check chunk upload success") + @Nested + class TestCheckChunkUploadSuccess { + + @DisplayName("should return") + @Test + void shouldReturn() { + var quarantineFileResult = QuarantineFileResultTestFactory.create(); + + mapper.checkChunkUploadSuccess(quarantineFileResult); + } + + @DisplayName("should throw on failed upload") + @Test + void shouldThrowOnFailedUpload() { + var quarantineFileResult = QuarantineFileResultTestFactory.createBuilder() + .success(false) + .error(ERROR_STRING) + .fileUid(Osi2FileUploadTestFactory.UPLOAD_GUID) + .build(); + + assertThatThrownBy(() -> mapper.checkChunkUploadSuccess(quarantineFileResult)) + .isInstanceOf(Osi2RuntimeException.class) + .hasMessageContaining(ERROR_STRING) + .hasMessageContaining(Osi2FileUploadTestFactory.UPLOAD_GUID); + } } } 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 d2ceb286eaf9a6dfe46d5f79841fa0f63b08ea18..0e6e97f9263715e2f246471e0364cf0e351a6e03 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 @@ -2,7 +2,7 @@ package de.ozgcloud.nachrichten.postfach.osiv2.transfer; import static de.ozgcloud.nachrichten.postfach.osiv2.factory.MessageExchangeReceiveMessagesResponseTestFactory.*; import static de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachAddressTestFactory.*; -import static de.ozgcloud.nachrichten.postfach.osiv2.transfer.PostfachApiFacadeService.*; +import static de.ozgcloud.nachrichten.postfach.osiv2.transfer.Osi2RequestMapper.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -16,37 +16,54 @@ import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; +import org.springframework.core.io.AbstractResource; import de.ozgcloud.nachrichten.postfach.PostfachNachricht; -import de.ozgcloud.nachrichten.postfach.osiv2.OsiPostfachException; +import de.ozgcloud.nachrichten.postfach.osiv2.config.Osi2PostfachProperties; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.FileChunkInfoTestFactory; import de.ozgcloud.nachrichten.postfach.osiv2.factory.MessageExchangeReceiveMessagesResponseTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2FileUploadTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.Osi2MessageTestFactory; import de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.V1ReplyMessageTestFactory; import de.ozgcloud.nachrichten.postfach.osiv2.gen.api.MessageExchangeApi; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.api.QuarantineApi; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.DomainChunkMetaData; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeDeleteMessageResponse; -import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeReceiveMessage; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeReceiveMessagesResponse; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.MessageExchangeSendMessageResponse; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.OutSendMessageRequestV2; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.QuarantineStatus; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyMessage; +import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Attachment; +import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2Message; +import lombok.SneakyThrows; class PostfachApiFacadeServiceTest { @InjectMocks @Spy - PostfachApiFacadeService postfachApiFacadeService; + PostfachApiFacadeService service; @Mock MessageExchangeApi messageExchangeApi; + @Mock + QuarantineApi quarantineApi; + @Mock Osi2RequestMapper osi2RequestMapper; @Mock Osi2ResponseMapper osi2ResponseMapper; + @Mock + Osi2PostfachProperties.ApiConfiguration apiConfiguration; + @DisplayName("send message") @Nested - class TestSendMessage { + class TestSendOsi2Message { @Mock OutSendMessageRequestV2 outSendMessageRequestV2; @@ -54,28 +71,46 @@ class PostfachApiFacadeServiceTest { @Mock MessageExchangeSendMessageResponse messageExchangeSendMessageResponse; + private final List<Osi2Attachment> files = List.of(Osi2FileUploadTestFactory.create()); + private final PostfachNachricht nachricht = PostfachNachrichtTestFactory.create(); @BeforeEach void mock() { - when(osi2RequestMapper.mapMailboxId(nachricht)).thenReturn(MAILBOX_ID); - when(osi2RequestMapper.mapOutSendMessageRequestV2(nachricht)).thenReturn(outSendMessageRequestV2); + when(osi2RequestMapper.mapMailboxId(any())).thenReturn(MAILBOX_ID); + when(osi2RequestMapper.mapOutSendMessageRequestV2(any(), any())).thenReturn(outSendMessageRequestV2); when(messageExchangeApi.sendMessage(any(), any())).thenReturn(messageExchangeSendMessageResponse); } + @DisplayName("should call mapMailboxId") + @Test + void shouldCallMapMailboxId() { + service.sendMessage(nachricht, files); + + verify(osi2RequestMapper).mapMailboxId(nachricht); + } + + @DisplayName("should call mapOutSendMessageRequestV2") + @Test + void shouldCallMapOutSendMessageRequestV2() { + service.sendMessage(nachricht, files); + + verify(osi2RequestMapper).mapOutSendMessageRequestV2(nachricht, files); + } + @DisplayName("should call sendMessage") @Test void shouldCallSendMessage() { - postfachApiFacadeService.sendMessage(nachricht); + service.sendMessage(nachricht, files); verify(messageExchangeApi).sendMessage(MAILBOX_ID, outSendMessageRequestV2); - } + } - @DisplayName("receive messages") + @DisplayName("fetch pending message ids") @Nested - class TestReceiveMessage { + class TestFetchPendingOsi2MessageIds { @DisplayName("with two pending messages") @Nested @@ -87,26 +122,29 @@ class PostfachApiFacadeServiceTest { @BeforeEach void mock() { when(messageExchangeApi.receiveMessages(anyInt(), anyInt())).thenReturn(response); - doReturn(PostfachNachrichtTestFactory.createBuilder().messageId(MESSAGE_ID_1).build()) - .when(postfachApiFacadeService).fetchMessageByGuid(response.getMessages().get(0)); - doReturn(PostfachNachrichtTestFactory.createBuilder().messageId(MESSAGE_ID_2).build()) - .when(postfachApiFacadeService).fetchMessageByGuid(response.getMessages().get(1)); + when(osi2ResponseMapper.toMessageIds(any())).thenReturn(List.of(MESSAGE_ID_1, MESSAGE_ID_2)); } @DisplayName("should return") @Test void shouldReturn() { - var messages = receiveMessageList(); + var messageIds = fetchMessageIds(); + + assertThat(messageIds).containsExactly(MESSAGE_ID_1, MESSAGE_ID_2); + } + + @DisplayName("should call mapper") + @Test + void shouldCallMapper() { + fetchMessageIds(); - assertThat(messages) - .extracting(PostfachNachricht::getMessageId) - .containsExactly(MESSAGE_ID_1, MESSAGE_ID_2); + verify(osi2ResponseMapper).toMessageIds(response); } @DisplayName("should call receiveMessages api method") @Test void shouldCallReceiveMessagesApiMethod() { - receiveMessageList(); + fetchMessageIds(); verify(messageExchangeApi).receiveMessages(MAX_NUMBER_RECEIVED_MESSAGES, 0); } @@ -126,73 +164,140 @@ class PostfachApiFacadeServiceTest { @DisplayName("should return") @Test void shouldReturn() { - var messages = receiveMessageList(); + var messages = fetchMessageIds(); assertThat(messages).isEmpty(); } } - @DisplayName("with null response") - @Nested - class TestWithNullResponse { - - @DisplayName("should throw") - @Test - void shouldThrow() { - assertThatThrownBy(TestReceiveMessage.this::receiveMessageList) - .isInstanceOf(OsiPostfachException.class); - } - } - - private List<PostfachNachricht> receiveMessageList() { - return postfachApiFacadeService.receiveMessages().toList(); + private List<String> fetchMessageIds() { + return service.fetchPendingMessageIds(); } } - @DisplayName("fetch Message by guid") + @DisplayName("fetch message by id") @Nested - class TestFetchMessageByGuid { + class TestFetchMessageById { - @Mock - V1ReplyMessage replyMessage; - @Mock - MessageExchangeReceiveMessage receiveMessage; + private final V1ReplyMessage replyMessage = V1ReplyMessageTestFactory.create(); + private final Osi2Message message = Osi2MessageTestFactory.create(); @Test void shouldCallGetMessage() { when(messageExchangeApi.getMessage(any())).thenReturn(replyMessage); - postfachApiFacadeService.fetchMessageByGuid(receiveMessage); + service.fetchMessageById(MESSAGE_ID_1); - verify(messageExchangeApi).getMessage(any()); + verify(messageExchangeApi).getMessage(UUID.fromString(MESSAGE_ID_1)); } @Test void shouldCallResponseMapper() { when(messageExchangeApi.getMessage(any())).thenReturn(replyMessage); - when(osi2ResponseMapper.toPostfachNachricht(any())).thenReturn(PostfachNachrichtTestFactory.create()); + when(osi2ResponseMapper.toMessage(any())).thenReturn(Osi2MessageTestFactory.create()); - postfachApiFacadeService.fetchMessageByGuid(receiveMessage); + service.fetchMessageById(MESSAGE_ID_1); - verify(osi2ResponseMapper).toPostfachNachricht(any()); + verify(osi2ResponseMapper).toMessage(replyMessage); } + @DisplayName("should return") @Test - void shouldReturnPostfachNachricht() { + void shouldReturn() { when(messageExchangeApi.getMessage(any())).thenReturn(replyMessage); - when(osi2ResponseMapper.toPostfachNachricht(any())).thenReturn(PostfachNachrichtTestFactory.create()); + when(osi2ResponseMapper.toMessage(any())).thenReturn(message); + + var result = service.fetchMessageById(MESSAGE_ID_1); + + assertThat(result).isEqualTo(message); + } + } + + @DisplayName("upload chunk") + @Nested + class TestUploadChunk { + @Mock + AbstractResource chunkResource; + + @Mock + DomainChunkMetaData domainChunkMetaData; + + private static final String TENANT = "tenant"; + private static final String NAME_IDENTIFIER = "nameIdentifier"; - var postfachNachricht = postfachApiFacadeService.fetchMessageByGuid(receiveMessage); + private final FileChunkInfo info = FileChunkInfoTestFactory.create(); - assertThat(postfachNachricht).isInstanceOf(PostfachNachricht.class); + @BeforeEach + void mock() { + when(apiConfiguration.getTenant()).thenReturn(TENANT); + when(apiConfiguration.getNameIdentifier()).thenReturn(NAME_IDENTIFIER); + when(osi2RequestMapper.mapDomainChunkMetaData(any())).thenReturn(domainChunkMetaData); + } + + @DisplayName("should call mapDomainChunkMetadata") + @Test + void shouldCallMapDomainChunkMetadata() { + service.uploadChunk(info, chunkResource); + + verify(osi2RequestMapper).mapDomainChunkMetaData(info); + } + + @DisplayName("should call uploadChunk") + @Test + void shouldCallUploadChunk() { + service.uploadChunk(info, chunkResource); + + verify(quarantineApi).uploadChunk(domainChunkMetaData, TENANT, NAME_IDENTIFIER, chunkResource); } } - @DisplayName("Delete Message") + @DisplayName("check upload successful") @Nested - class TestDeleteMessage { + class TestCheckUploadSuccessful { + + private final QuarantineStatus uploadStatus = QuarantineStatus.SAFE; + + @BeforeEach + @SneakyThrows + void mock() { + when(osi2ResponseMapper.isSafe(any())).thenReturn(true); + when(quarantineApi.getUploadStatus(any())).thenReturn(uploadStatus); + } + + @DisplayName("should call getUploadStatus") + @Test + @SneakyThrows + void shouldCallGetUploadStatus() { + service.checkUploadSuccessful(MESSAGE_ID_1); + + verify(quarantineApi).getUploadStatus(UUID.fromString(MESSAGE_ID_1)); + } + + @DisplayName("should call isSafe") + @Test + @SneakyThrows + void shouldCallIsSafe() { + service.checkUploadSuccessful(MESSAGE_ID_1); + + verify(osi2ResponseMapper).isSafe(uploadStatus); + } + + @DisplayName("should return true") + @Test + @SneakyThrows + void shouldReturnTrue() { + var result = service.checkUploadSuccessful(MESSAGE_ID_1); + + assertThat(result).isTrue(); + } + + } + + @DisplayName("delete message") + @Nested + class TestDeleteOsi2Message { @Mock MessageExchangeDeleteMessageResponse replyMessage; @@ -200,9 +305,21 @@ class PostfachApiFacadeServiceTest { void shouldCallDeleteMessage() { when(messageExchangeApi.deleteMessage(any())).thenReturn(replyMessage); - postfachApiFacadeService.deleteMessage(UUID.randomUUID().toString()); + service.deleteMessage(MESSAGE_ID_1); + + verify(messageExchangeApi).deleteMessage(UUID.fromString(MESSAGE_ID_1)); + } + } + + @DisplayName("delete file upload") + @Nested + class TestDeleteFileUpload { + @DisplayName("should call deleteUpload") + @Test + void shouldCallDeleteUpload() { + service.deleteFileUpload(MESSAGE_ID_1); - verify(messageExchangeApi).deleteMessage(any()); + verify(quarantineApi).deleteUpload(UUID.fromString(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 0000000000000000000000000000000000000000..c33e3e9d55fed284c6f9cd3037c226a344d9844a --- /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 diff --git a/src/test/resources/application-itcase.yml b/src/test/resources/application-itcase.yml index 41e84786e9e76db63c2d6671a55ae56dddae6ea7..37a4164e12939e2ce5689d44b0edf53d7cc01143 100644 --- a/src/test/resources/application-itcase.yml +++ b/src/test/resources/application-itcase.yml @@ -5,4 +5,11 @@ logging: level: de.ozgcloud.nachrichten.postfach.osiv2: DEBUG org.springframework.http: DEBUG - org.springframework.web.client: DEBUG \ No newline at end of file + org.springframework.web.client: DEBUG + +spring: + grpc: + client: + file-manager: + address: static://127.0.0.1:9090 + negotiationType: PLAINTEXT \ No newline at end of file