diff --git a/iris-client-bff/pom.xml b/iris-client-bff/pom.xml index 805b0bf32..5625982e6 100644 --- a/iris-client-bff/pom.xml +++ b/iris-client-bff/pom.xml @@ -229,6 +229,11 @@ org.apache.httpcomponents httpclient + + org.apache.tika + tika-core + 2.2.0 + org.projectlombok lombok diff --git a/iris-client-bff/src/main/java/iris/client_bff/config/DataSubmissionConfig.java b/iris-client-bff/src/main/java/iris/client_bff/config/DataSubmissionConfig.java index 790825bbc..fba92be4f 100644 --- a/iris-client-bff/src/main/java/iris/client_bff/config/DataSubmissionConfig.java +++ b/iris-client-bff/src/main/java/iris/client_bff/config/DataSubmissionConfig.java @@ -2,6 +2,7 @@ import iris.client_bff.cases.eps.CaseDataController; import iris.client_bff.events.eps.EventDataController; +import iris.client_bff.iris_messages.eps.IrisMessageDataController; import lombok.AllArgsConstructor; import org.springframework.context.annotation.Bean; @@ -21,6 +22,8 @@ public class DataSubmissionConfig { EventDataController eventDataController; + IrisMessageDataController irisMessageDataController; + @Bean(name = DATA_SUBMISSION_ENDPOINT) public CompositeJsonServiceExporter jsonRpcServiceImplExporter() { return createCompositeJsonServiceExporter(); @@ -34,7 +37,7 @@ public CompositeJsonServiceExporter jsonRpcServiceImplExporterWithSlash() { private CompositeJsonServiceExporter createCompositeJsonServiceExporter() { CompositeJsonServiceExporter compositeJsonServiceExporter = new CompositeJsonServiceExporter(); - compositeJsonServiceExporter.setServices(new Object[] { caseDataController, eventDataController }); + compositeJsonServiceExporter.setServices(new Object[] { caseDataController, eventDataController, irisMessageDataController }); compositeJsonServiceExporter.setAllowExtraParams(true); // Used to allow the EPS to add common parameters (e.g. a signature) and not have to change all methods. return compositeJsonServiceExporter; diff --git a/iris-client-bff/src/main/java/iris/client_bff/config/HibernateSearchConfig.java b/iris-client-bff/src/main/java/iris/client_bff/config/HibernateSearchConfig.java index 97241ca82..a571a9d03 100644 --- a/iris-client-bff/src/main/java/iris/client_bff/config/HibernateSearchConfig.java +++ b/iris-client-bff/src/main/java/iris/client_bff/config/HibernateSearchConfig.java @@ -4,6 +4,8 @@ import iris.client_bff.cases.CaseDataRequest.DataRequestIdentifier; import iris.client_bff.events.EventDataRequest; import iris.client_bff.events.model.Location.LocationIdentifier; +import iris.client_bff.iris_messages.IrisMessage; +import iris.client_bff.iris_messages.IrisMessageFolder; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -24,8 +26,10 @@ import org.hibernate.search.mapper.orm.massindexing.MassIndexer; import org.hibernate.search.mapper.orm.session.SearchSession; import org.hibernate.search.mapper.pojo.bridge.IdentifierBridge; +import org.hibernate.search.mapper.pojo.bridge.ValueBridge; import org.hibernate.search.mapper.pojo.bridge.runtime.IdentifierBridgeFromDocumentIdentifierContext; import org.hibernate.search.mapper.pojo.bridge.runtime.IdentifierBridgeToDocumentIdentifierContext; +import org.hibernate.search.mapper.pojo.bridge.runtime.ValueBridgeToIndexedValueContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; @@ -138,6 +142,58 @@ public LocationIdentifier fromDocumentIdentifier(String value, return value == null ? null : LocationIdentifier.of(value); } }); + + context.bridges().exactType(IrisMessage.IrisMessageIdentifier.class) + .identifierBridge(new IdentifierBridge<>() { + + @Override + public String toDocumentIdentifier( + IrisMessage.IrisMessageIdentifier value, + IdentifierBridgeToDocumentIdentifierContext context + ) { + return value.toString(); + } + + @Override + public IrisMessage.IrisMessageIdentifier fromDocumentIdentifier( + String value, + IdentifierBridgeFromDocumentIdentifierContext context + ) { + return value == null ? null : IrisMessage.IrisMessageIdentifier.of(value); + } + }); + + context.bridges().exactType(IrisMessageFolder.IrisMessageFolderIdentifier.class) + .identifierBridge(new IdentifierBridge<>() { + + @Override + public String toDocumentIdentifier( + IrisMessageFolder.IrisMessageFolderIdentifier value, + IdentifierBridgeToDocumentIdentifierContext context + ) { + return value.toString(); + } + + @Override + public IrisMessageFolder.IrisMessageFolderIdentifier fromDocumentIdentifier( + String value, + IdentifierBridgeFromDocumentIdentifierContext context + ) { + return value == null ? null : IrisMessageFolder.IrisMessageFolderIdentifier.of(value); + } + }); + + context.bridges().exactType(IrisMessageFolder.IrisMessageFolderIdentifier.class) + .valueBridge(new ValueBridge() { + @Override + public String toIndexedValue( + IrisMessageFolder.IrisMessageFolderIdentifier value, + ValueBridgeToIndexedValueContext context + ) { + return value == null ? null : value.toString(); + } + }); + } } } diff --git a/iris-client-bff/src/main/java/iris/client_bff/core/utils/HibernateSearcher.java b/iris-client-bff/src/main/java/iris/client_bff/core/utils/HibernateSearcher.java index da8bc8d37..8e76ced87 100644 --- a/iris-client-bff/src/main/java/iris/client_bff/core/utils/HibernateSearcher.java +++ b/iris-client-bff/src/main/java/iris/client_bff/core/utils/HibernateSearcher.java @@ -40,7 +40,7 @@ public SearchResult search(String search, Pageable pageable, String[] fie } private PredicateFinalStep createQuery(String keyword, String[] fields, - UnaryOperator> statusMatchFunc, SearchPredicateFactory f) { + UnaryOperator> searchInterceptor, SearchPredicateFactory f) { var boolPred = f.bool(); @@ -48,7 +48,7 @@ private PredicateFinalStep createQuery(String keyword, String[] fields, boolPred = boolPred.must(f2 -> createQueryPart(keywordPart, fields, f2)); } - return statusMatchFunc.apply(boolPred); + return searchInterceptor.apply(boolPred); } private PredicateFinalStep createQueryPart(String keyword, String[] fields, SearchPredicateFactory f) { diff --git a/iris-client-bff/src/main/java/iris/client_bff/core/web/error/GlobalControllerExceptionHandler.java b/iris-client-bff/src/main/java/iris/client_bff/core/web/error/GlobalControllerExceptionHandler.java index 07aab7421..9e6969bf6 100644 --- a/iris-client-bff/src/main/java/iris/client_bff/core/web/error/GlobalControllerExceptionHandler.java +++ b/iris-client-bff/src/main/java/iris/client_bff/core/web/error/GlobalControllerExceptionHandler.java @@ -1,25 +1,33 @@ package iris.client_bff.core.web.error; +import static org.apache.commons.lang3.ArrayUtils.*; import static org.apache.commons.lang3.StringUtils.*; import iris.client_bff.core.web.filter.ApplicationRequestSizeLimitFilter.BlockLimitExceededException; import iris.client_bff.events.exceptions.IRISDataRequestException; import iris.client_bff.search_client.exceptions.IRISSearchException; import iris.client_bff.ui.messages.ErrorMessages; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.support.MessageSourceAccessor; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.HttpStatus; +import org.springframework.util.unit.DataSize; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.multipart.MaxUploadSizeExceededException; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @Slf4j @ControllerAdvice +@RequiredArgsConstructor public class GlobalControllerExceptionHandler extends ResponseEntityExceptionHandler { + private final MessageSourceAccessor messages; + @ResponseStatus(value = HttpStatus.SERVICE_UNAVAILABLE, reason = ErrorMessages.LOCATION_SEARCH) @ExceptionHandler(IRISSearchException.class) public void handleIRISSearchException(IRISSearchException ex) { @@ -48,6 +56,16 @@ public void handleException(Exception ex) throws Exception { throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, getInternalMessage(ex)); } + @ExceptionHandler(MaxUploadSizeExceededException.class) + public void handleMaxUploadSizeExceededException(MaxUploadSizeExceededException ex) { + + var maxSize = DataSize.ofBytes(ex.getMaxUploadSize()); + + throw new ResponseStatusException( + HttpStatus.PAYLOAD_TOO_LARGE, + messages.getMessage("iris_message.max_upload_file_size", toArray(maxSize.toMegabytes() + " MB"))); + } + private String getInternalMessage(Exception ex) { var msg = defaultString(ex.getMessage()); diff --git a/iris-client-bff/src/main/java/iris/client_bff/hd_search/HdSearchException.java b/iris-client-bff/src/main/java/iris/client_bff/hd_search/HdSearchException.java new file mode 100644 index 000000000..0d205fe7f --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/hd_search/HdSearchException.java @@ -0,0 +1,27 @@ +package iris.client_bff.hd_search; + +import org.apache.commons.lang3.exception.ExceptionUtils; + +import java.io.Serial; + +public class HdSearchException extends RuntimeException { + + @Serial + private static final long serialVersionUID = -3649251457000674951L; + + public HdSearchException(String failedMethod, Throwable cause) { + super("Call to '" + failedMethod + "' failed", cause); + } + + public HdSearchException(String message) { + super(message); + } + + public HdSearchException(Throwable cause) { + super(cause); + } + + public String getErrorMessage() { + return ExceptionUtils.getRootCause(this).getMessage(); + } +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/hd_search/HealthDepartment.java b/iris-client-bff/src/main/java/iris/client_bff/hd_search/HealthDepartment.java new file mode 100644 index 000000000..de43504d5 --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/hd_search/HealthDepartment.java @@ -0,0 +1,36 @@ +package iris.client_bff.hd_search; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class HealthDepartment { + + private String name; + private String rkiCode; + private String epsName; + private String department; + private Address address; + private ContactData contactData; + private ContactData covid19ContactData; + private ContactData entryContactData; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + private class Address { + private String street; + private String zipCode; + private String city; + } + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + private class ContactData { + private String phone; + private String fax; + @JsonProperty("eMail") + private String eMail; + } +} \ No newline at end of file diff --git a/iris-client-bff/src/main/java/iris/client_bff/hd_search/eps/EPSHdSearchClient.java b/iris-client-bff/src/main/java/iris/client_bff/hd_search/eps/EPSHdSearchClient.java new file mode 100644 index 000000000..7b7d83a42 --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/hd_search/eps/EPSHdSearchClient.java @@ -0,0 +1,34 @@ +package iris.client_bff.hd_search.eps; + +import com.googlecode.jsonrpc4j.JsonRpcHttpClient; +import iris.client_bff.config.BackendServiceProperties; +import iris.client_bff.hd_search.HdSearchException; +import iris.client_bff.hd_search.HealthDepartment; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@Slf4j +@Service +@AllArgsConstructor +public class EPSHdSearchClient { + + private final JsonRpcHttpClient epsRpcClient; + private BackendServiceProperties config; + + public List searchForHd(String search) { + + var methodName = config.getEndpoint() + ".searchForHd"; + Map payload = Map.of("searchKeyword", search); + + try { + return Arrays.stream(epsRpcClient.invoke(methodName, payload, HealthDepartment[].class)).toList(); + } catch (Throwable t) { + throw new HdSearchException(methodName, t); + } + } +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessage.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessage.java new file mode 100644 index 000000000..04d0a90ea --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessage.java @@ -0,0 +1,94 @@ +package iris.client_bff.iris_messages; + +import iris.client_bff.core.Aggregate; +import iris.client_bff.core.Id; +import lombok.*; +import org.hibernate.search.engine.backend.types.Sortable; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.*; + +import javax.persistence.*; +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "iris_message") +@Indexed +@Data +@EqualsAndHashCode(callSuper = true, exclude = "folder") +@NoArgsConstructor +public class IrisMessage extends Aggregate { + + public static final int SUBJECT_MAX_LENGTH = 500; + public static final int BODY_MAX_LENGTH = 6000; + + { + id = IrisMessage.IrisMessageIdentifier.of(UUID.randomUUID()); + } + + @ManyToOne + @JoinColumn(name="folder_id", nullable=false) + @IndexedEmbedded(includeEmbeddedObjectId = true) + private IrisMessageFolder folder; + + @Column(nullable = false) + @KeywordField(sortable = Sortable.YES, normalizer = "german") + private String subject; + + @Column(nullable = false) + private String body; + + @Column(nullable = false) + @Embedded + @IndexedEmbedded + @AttributeOverrides({ + @AttributeOverride( name = "id", column = @Column(name = "hd_author_id")), + @AttributeOverride( name = "name", column = @Column(name = "hd_author_name")) + }) + private IrisMessageHdContact hdAuthor; + + @Column(nullable = false) + @Embedded + @IndexedEmbedded + @AttributeOverrides({ + @AttributeOverride( name = "id", column = @Column(name = "hd_recipient_id")), + @AttributeOverride( name = "name", column = @Column(name = "hd_recipient_name")) + }) + private IrisMessageHdContact hdRecipient; + + private Boolean isRead; + + @OneToMany(mappedBy = "message", cascade = CascadeType.ALL, orphanRemoval = true) + private List fileAttachments = new ArrayList<>(); + + @Embeddable + @EqualsAndHashCode + @RequiredArgsConstructor(staticName = "of") + @NoArgsConstructor(force = true, access = AccessLevel.PRIVATE) + public static class IrisMessageIdentifier implements Id, Serializable { + + @Serial + private static final long serialVersionUID = 1140444389070674189L; + + private final UUID id; + + /** + * for JSON deserialization + */ + public static IrisMessage.IrisMessageIdentifier of(String uuid) { + return of(UUID.fromString(uuid)); + } + + @Override + public String toString() { + return id.toString(); + } + + public UUID toUUID() { + return id; + } + } + +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageBuilder.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageBuilder.java new file mode 100644 index 000000000..3d0775c4e --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageBuilder.java @@ -0,0 +1,113 @@ +package iris.client_bff.iris_messages; + +import iris.client_bff.iris_messages.eps.EPSIrisMessageClient; +import iris.client_bff.iris_messages.eps.IrisMessageTransferDto; +import iris.client_bff.iris_messages.web.IrisMessageInsertDto; +import iris.client_bff.iris_messages.web.IrisMessageInsertFileDto; +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class IrisMessageBuilder { + + private final IrisMessageFolderRepository folderRepository; + private final EPSIrisMessageClient irisMessageClient; + private final MessageSourceAccessor messages; + + public IrisMessage build(IrisMessageTransferDto messageTransfer) throws IrisMessageException { + + Optional folder = this.folderRepository + .findFirstByContextAndParentFolderIsNull(IrisMessageContext.INBOX); + if (folder.isEmpty()) { + throw new IrisMessageException(messages.getMessage("iris_message.invalid_folder")); + } + + IrisMessageHdContact hdAuthor = new IrisMessageHdContact( + messageTransfer.getHdAuthor().getId(), + messageTransfer.getHdAuthor().getName()); + + IrisMessageHdContact hdRecipient = new IrisMessageHdContact( + messageTransfer.getHdRecipient().getId(), + messageTransfer.getHdRecipient().getName()); + + // ensure that the message was sent to the correct recipient + IrisMessageHdContact hdOwn = this.irisMessageClient.getOwnIrisMessageHdContact(); + if (!Objects.equals(hdOwn.getId(), hdRecipient.getId())) { + throw new IrisMessageException(messages.getMessage("iris_message.invalid_recipient")); + } + + IrisMessage message = new IrisMessage(); + + List files = new ArrayList<>(); + if (messageTransfer.getFileAttachments() != null) { + for ( IrisMessageTransferDto.FileAttachment file : messageTransfer.getFileAttachments() ) { + IrisMessageFile messageFile = new IrisMessageFile() + .setMessage(message) + .setContent(Base64.getDecoder().decode(file.getContent())) + .setName(file.getName()); + files.add(messageFile); + } + } + + message + .setHdAuthor(hdAuthor) + .setHdRecipient(hdRecipient) + .setSubject(messageTransfer.getSubject()) + .setBody(messageTransfer.getBody()) + .setFolder(folder.get()) + .setIsRead(false) + .setFileAttachments(files); + + return message; + } + + public IrisMessage build(IrisMessageInsertDto messageInsert) throws IrisMessageException { + + Optional folder = this.folderRepository + .findFirstByContextAndParentFolderIsNull(IrisMessageContext.OUTBOX); + if (folder.isEmpty()) { + throw new IrisMessageException("iris_message.invalid_folder"); + } + + IrisMessageHdContact hdAuthor = this.irisMessageClient.getOwnIrisMessageHdContact(); + + Optional hdRecipient = this.irisMessageClient + .findIrisMessageHdContactById(messageInsert.getHdRecipient()); + if (hdRecipient.isEmpty()) { + throw new IrisMessageException("iris_message.invalid_recipient"); + } + + IrisMessage message = new IrisMessage(); + + List files = new ArrayList<>(); + if (messageInsert.getFileAttachments() != null) { + for ( IrisMessageInsertFileDto file : messageInsert.getFileAttachments() ) { + IrisMessageFile messageFile = new IrisMessageFile() + .setMessage(message) + .setContent(Base64.getDecoder().decode(file.getContent())) + .setName(file.getName()); + files.add(messageFile); + } + } + + message + .setHdAuthor(hdAuthor) + .setHdRecipient(hdRecipient.get()) + .setSubject(messageInsert.getSubject()) + .setBody(messageInsert.getBody()) + .setFolder(folder.get()) + .setIsRead(true) + .setFileAttachments(files); + + return message; + } +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageContext.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageContext.java new file mode 100644 index 000000000..a70d5e4ca --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageContext.java @@ -0,0 +1,6 @@ +package iris.client_bff.iris_messages; + +public enum IrisMessageContext { + INBOX, + OUTBOX; +} \ No newline at end of file diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageException.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageException.java new file mode 100644 index 000000000..9a1f2b7e3 --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageException.java @@ -0,0 +1,26 @@ +package iris.client_bff.iris_messages; + +import org.apache.commons.lang3.exception.ExceptionUtils; + +import java.io.Serial; + +public class IrisMessageException extends RuntimeException { + @Serial + private static final long serialVersionUID = 8068203942662884747L; + + public IrisMessageException(String failedMethod, Throwable cause) { + super("Call to '" + failedMethod + "' failed", cause); + } + + public IrisMessageException(String message) { + super(message); + } + + public IrisMessageException(Throwable cause) { + super(cause); + } + + public String getErrorMessage() { + return ExceptionUtils.getRootCause(this).getMessage(); + } +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageFile.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageFile.java new file mode 100644 index 000000000..497e1576b --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageFile.java @@ -0,0 +1,63 @@ +package iris.client_bff.iris_messages; + +import iris.client_bff.core.Aggregate; +import iris.client_bff.core.Id; +import lombok.*; + +import javax.persistence.*; +import java.io.Serial; +import java.io.Serializable; +import java.util.UUID; + +@Entity +@Table(name = "iris_message_file") +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +public class IrisMessageFile extends Aggregate { + + public static final int NAME_MAX_LENGTH = 255; + + { + id = IrisMessageFileIdentifier.of(UUID.randomUUID()); + } + + @Column(nullable = false) + private String name; + + @Column(length = 16777215) + private byte[] content; + + @ToString.Exclude + @ManyToOne + @JoinColumn(name="message_id") + private IrisMessage message; + + @Embeddable + @EqualsAndHashCode + @RequiredArgsConstructor(staticName = "of") + @NoArgsConstructor(force = true, access = AccessLevel.PROTECTED) + public static class IrisMessageFileIdentifier implements Id, Serializable { + + @Serial + private static final long serialVersionUID = -7602440129090196288L; + + private final UUID id; + + /** + * for JSON deserialization + */ + public static IrisMessageFile.IrisMessageFileIdentifier of(String uuid) { + return of(UUID.fromString(uuid)); + } + + @Override + public String toString() { + return id.toString(); + } + + public UUID toUUID() { + return id; + } + } +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageFileRepository.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageFileRepository.java new file mode 100644 index 000000000..729f42414 --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageFileRepository.java @@ -0,0 +1,8 @@ +package iris.client_bff.iris_messages; + +import iris.client_bff.iris_messages.IrisMessageFile.IrisMessageFileIdentifier; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface IrisMessageFileRepository + extends JpaRepository {} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageFolder.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageFolder.java new file mode 100644 index 000000000..83277dbfc --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageFolder.java @@ -0,0 +1,81 @@ +package iris.client_bff.iris_messages; + +import iris.client_bff.core.Aggregate; +import iris.client_bff.core.Id; +import lombok.AccessLevel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Set; +import java.util.UUID; + +import javax.persistence.*; + +@Entity +@Table(name = "iris_message_folder") +@SecondaryTable(name = "iris_message_folder_default") +@Data +@EqualsAndHashCode(callSuper = true, exclude = "messages") +@NoArgsConstructor +public class IrisMessageFolder extends Aggregate { + + { + id = IrisMessageFolder.IrisMessageFolderIdentifier.of(UUID.randomUUID()); + } + + @ToString.Exclude + @OneToMany(mappedBy = "folder", cascade = CascadeType.ALL, orphanRemoval = true) + private Set messages; + + @Column(nullable = false) + private String name; + + @Enumerated(EnumType.STRING) + private IrisMessageContext context; + + @Embedded + @AttributeOverride(name = "id", + column = @Column(name = "id", table = "iris_message_folder_default", insertable = false, updatable = false)) + private IrisMessageFolderIdentifier defaultFolder; + + @Embedded + @AttributeOverride(name = "id", column = @Column(name = "parent_folder")) + private IrisMessageFolderIdentifier parentFolder; + + public Boolean getIsDefault() { + return this.getId().equals(this.defaultFolder); + } + + @Embeddable + @EqualsAndHashCode + @RequiredArgsConstructor(staticName = "of") + @NoArgsConstructor(force = true, access = AccessLevel.PRIVATE) + public static class IrisMessageFolderIdentifier implements Id, Serializable { + + @Serial + private static final long serialVersionUID = -8255216015747810442L; + + final UUID id; + + /** + * for JSON deserialization + */ + public static IrisMessageFolder.IrisMessageFolderIdentifier of(String uuid) { + return of(UUID.fromString(uuid)); + } + + @Override + public String toString() { + return id.toString(); + } + + public UUID toUUID() { + return id; + } + } +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageFolderInitializer.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageFolderInitializer.java new file mode 100644 index 000000000..bb27a719a --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageFolderInitializer.java @@ -0,0 +1,36 @@ +package iris.client_bff.iris_messages; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.PostConstruct; + +import org.springframework.stereotype.Component; + +@Component +@AllArgsConstructor +@Slf4j +public class IrisMessageFolderInitializer { + + private IrisMessageFolderRepository folderRepository; + + @PostConstruct + protected void createMessageFoldersIfNotExist() { + if (folderRepository.count() == 0) { + IrisMessageFolder inboxFolder = new IrisMessageFolder(); + inboxFolder + .setName("Posteingang") + .setDefaultFolder(inboxFolder.getId()) + .setContext(IrisMessageContext.INBOX); + IrisMessageFolder outboxFolder = new IrisMessageFolder(); + outboxFolder + .setName("Postausgang") + .setDefaultFolder(inboxFolder.getId()) + .setContext(IrisMessageContext.OUTBOX); + folderRepository.save(inboxFolder); + folderRepository.save(outboxFolder); + } else { + log.info("Initial iris message folders already exists. Skip creating of folders."); + } + } +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageFolderRepository.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageFolderRepository.java new file mode 100644 index 000000000..f86134cf7 --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageFolderRepository.java @@ -0,0 +1,16 @@ +package iris.client_bff.iris_messages; + +import org.springframework.data.jpa.repository.JpaRepository; + +import iris.client_bff.iris_messages.IrisMessageFolder.IrisMessageFolderIdentifier; + +import java.util.List; +import java.util.Optional; + +public interface IrisMessageFolderRepository extends JpaRepository { + + Optional findFirstByContextAndParentFolderIsNull(IrisMessageContext context); + + List findAllByParentFolder(IrisMessageFolderIdentifier parentFolder); + +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageHdContact.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageHdContact.java new file mode 100644 index 000000000..9ef310a7f --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageHdContact.java @@ -0,0 +1,36 @@ +package iris.client_bff.iris_messages; + +import lombok.*; +import org.hibernate.search.engine.backend.types.Sortable; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.KeywordField; + +import javax.persistence.Embeddable; +import javax.persistence.Transient; + +@Data +@Embeddable +@Builder +@NoArgsConstructor +public class IrisMessageHdContact { + + public static final int ID_MAX_LENGTH = 255; + public static final int NAME_MAX_LENGTH = 255; + + private String id; + @KeywordField(sortable = Sortable.YES, normalizer = "german") + private String name; + + @ToString.Exclude + @Transient + private Boolean isOwn; + + public IrisMessageHdContact(String id, String name) { + this.id = id; + this.name = name; + } + + public IrisMessageHdContact(String id, String name, Boolean isOwn) { + this(id, name); + this.isOwn = isOwn; + } +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageRepository.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageRepository.java new file mode 100644 index 000000000..c7b65e591 --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageRepository.java @@ -0,0 +1,17 @@ +package iris.client_bff.iris_messages; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface IrisMessageRepository extends JpaRepository { + + @Query("select count(m) from IrisMessage m where (m.isRead is null or m.isRead = false) and m.folder.id = :folderId") + int getCountUnreadByFolderId(IrisMessageFolder.IrisMessageFolderIdentifier folderId); + + int countByIsReadFalseOrIsReadIsNull(); + + Page findAllByFolderIdOrderByIsReadAsc(IrisMessageFolder.IrisMessageFolderIdentifier folder, Pageable pageable); + +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageService.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageService.java new file mode 100644 index 000000000..98fbd97a1 --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/IrisMessageService.java @@ -0,0 +1,99 @@ +package iris.client_bff.iris_messages; + +import iris.client_bff.core.utils.HibernateSearcher; +import iris.client_bff.hd_search.HealthDepartment; +import iris.client_bff.hd_search.eps.EPSHdSearchClient; +import iris.client_bff.iris_messages.IrisMessage.IrisMessageIdentifier; +import iris.client_bff.iris_messages.IrisMessageFolder.IrisMessageFolderIdentifier; +import iris.client_bff.iris_messages.eps.EPSIrisMessageClient; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class IrisMessageService { + + private static final String[] SEARCH_FIELDS = { "subject", "hdAuthor.name", "hdRecipient.name" }; + + private final IrisMessageRepository messageRepository; + private final IrisMessageFolderRepository folderRepository; + private final IrisMessageFileRepository fileRepository; + private final HibernateSearcher searcher; + private final EPSIrisMessageClient irisMessageClient; + private final EPSHdSearchClient hdSearchClient; + + public Optional findById(IrisMessageIdentifier messageId) { + return messageRepository.findById(messageId); + } + + public Page search(IrisMessageFolderIdentifier folderId, String searchString, Pageable pageable) { + if (StringUtils.isEmpty(searchString)) { + return messageRepository.findAllByFolderIdOrderByIsReadAsc(folderId, pageable); + } + var result = searcher.search( + searchString, + pageable, + SEARCH_FIELDS, + it -> it.must(f2 -> f2.match().field("folder.id").matching(folderId)), + IrisMessage.class); + return new PageImpl<>(result.hits(), pageable, result.total().hitCount()); + } + + public int getCountUnreadByFolderId(IrisMessageFolderIdentifier folderId) { + return messageRepository.getCountUnreadByFolderId(folderId); + } + + public int getCountUnread() { + return messageRepository.countByIsReadFalseOrIsReadIsNull(); + } + + public List getFolders() { + return folderRepository.findAll(); + } + + public Optional findFileById(IrisMessageFile.IrisMessageFileIdentifier fileId) { + return this.fileRepository.findById(fileId); + } + + public List getHdContacts(String search) throws IrisMessageException { + + List contacts = this.irisMessageClient.getIrisMessageHdContacts(); + + if (search == null || search.equals("")) { + return contacts; + } + + List healthDepartments = this.hdSearchClient.searchForHd(search); + List hdEpsNames = healthDepartments + .stream() + .map(HealthDepartment::getEpsName) + .filter(Objects::nonNull) + .toList(); + + return contacts + .stream() + .filter(contact -> hdEpsNames.contains(contact.getId()) || contact.getName().contains(search) || contact.getId().contains(search)) + .toList(); + } + + public IrisMessageHdContact getOwnHdContact() { + return this.irisMessageClient.getOwnIrisMessageHdContact(); + } + + public IrisMessage sendMessage(IrisMessage message) throws IrisMessageException { + this.irisMessageClient.createIrisMessage(message); + return this.messageRepository.save(message); + } + + public IrisMessage saveMessage(IrisMessage message) { + return this.messageRepository.save(message); + } + +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/eps/EPSIrisMessageClient.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/eps/EPSIrisMessageClient.java new file mode 100644 index 000000000..2b44541b6 --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/eps/EPSIrisMessageClient.java @@ -0,0 +1,73 @@ +package iris.client_bff.iris_messages.eps; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.googlecode.jsonrpc4j.JsonRpcHttpClient; +import iris.client_bff.config.RPCClientProperties; +import iris.client_bff.iris_messages.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.*; + +@Service +@RequiredArgsConstructor +public class EPSIrisMessageClient { + + private static final int READ_TIMEOUT = 12 * 1000; + + private final JsonRpcHttpClient epsRpcClient; + private final RPCClientProperties rpcClientProps; + + public IrisMessageHdContact getOwnIrisMessageHdContact() { + String ownId = rpcClientProps.getOwnEndpoint(); + return new IrisMessageHdContact(ownId, ownId, true); + } + + public Optional findIrisMessageHdContactById(String contactId) throws IrisMessageException { + List contacts = this.getIrisMessageHdContacts(); + return contacts.stream().filter(contact -> contact.getId().equals(contactId)).findFirst(); + } + + public List getIrisMessageHdContacts() throws IrisMessageException { + var methodName = rpcClientProps.getOwnEndpoint() + "._directory"; + try { + return epsRpcClient.invoke(methodName, null, Directory.class).entries().stream() + .filter(directoryEntry -> + directoryEntry.groups() != null && + directoryEntry.groups().contains("health-departments") && + directoryEntry.services() != null && + directoryEntry.services().stream().anyMatch(service -> service.name().equals("inter-ga-communication"))) + .map(directoryEntry -> new IrisMessageHdContact(directoryEntry.name, directoryEntry.name)) + .sorted(Comparator.comparing(IrisMessageHdContact::getName, String.CASE_INSENSITIVE_ORDER)) + .toList(); + } catch (Throwable t) { + throw new IrisMessageException(t); + } + } + + public void createIrisMessage(IrisMessage message) throws IrisMessageException { + String methodName = message.getHdRecipient().getId() + ".createIrisMessage"; + Map payload = Map.of("irisMessage", IrisMessageTransferDto.fromEntity(message)); + int defaultReadTimeout = this.epsRpcClient.getReadTimeoutMillis(); + try { + this.epsRpcClient.setReadTimeoutMillis(READ_TIMEOUT); + this.epsRpcClient.invoke(methodName, payload); + } catch (Throwable t) { + throw new IrisMessageException(t); + } finally { + this.epsRpcClient.setReadTimeoutMillis(defaultReadTimeout); + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + record Directory(@NotNull List<@Valid DirectoryEntry> entries) {} + + @JsonIgnoreProperties(ignoreUnknown = true) + record DirectoryEntry(@NotNull String name, Set groups, List<@Valid DirectoryEntryService> services) {} + + @JsonIgnoreProperties(ignoreUnknown = true) + record DirectoryEntryService(@NotNull String name) {} + +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/eps/IrisMessageDataController.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/eps/IrisMessageDataController.java new file mode 100644 index 000000000..33da87411 --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/eps/IrisMessageDataController.java @@ -0,0 +1,12 @@ +package iris.client_bff.iris_messages.eps; + +import com.googlecode.jsonrpc4j.JsonRpcParam; +import iris.client_bff.iris_messages.IrisMessageException; + +import javax.validation.Valid; + +public interface IrisMessageDataController { + IrisMessageTransferDto createIrisMessage( + @Valid @JsonRpcParam(value = "irisMessage") IrisMessageTransferDto messageTransfer + ) throws IrisMessageException; +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/eps/IrisMessageDataControllerImpl.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/eps/IrisMessageDataControllerImpl.java new file mode 100644 index 000000000..fe016950b --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/eps/IrisMessageDataControllerImpl.java @@ -0,0 +1,38 @@ +package iris.client_bff.iris_messages.eps; + +import iris.client_bff.config.JsonRpcDataValidator; +import iris.client_bff.iris_messages.IrisMessage; +import iris.client_bff.iris_messages.IrisMessageBuilder; +import iris.client_bff.iris_messages.IrisMessageException; +import iris.client_bff.iris_messages.IrisMessageService; +import lombok.RequiredArgsConstructor; + +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class IrisMessageDataControllerImpl implements IrisMessageDataController { + + private final IrisMessageTransferDefuse messageTransferDefuse; + private final IrisMessageService irisMessageService; + private final IrisMessageBuilder irisMessageBuilder; + private final MessageSourceAccessor messages; + + private final JsonRpcDataValidator jsonRpcDataValidator; + + @Override + public IrisMessageTransferDto createIrisMessage(IrisMessageTransferDto messageTransfer) throws IrisMessageException { + if (messageTransfer == null) { + throw new IrisMessageException(messages.getMessage("iris_message.invalid_id")); + } + try { + jsonRpcDataValidator.validateData(messageTransfer); + IrisMessage message = this.irisMessageBuilder.build(this.messageTransferDefuse.defuse(messageTransfer)); + IrisMessage savedMessage = this.irisMessageService.saveMessage(message); + return IrisMessageTransferDto.fromEntity(savedMessage); + } catch (Throwable e) { + throw new IrisMessageException(e); + } + } +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/eps/IrisMessageTransferDefuse.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/eps/IrisMessageTransferDefuse.java new file mode 100644 index 000000000..7eb6fa186 --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/eps/IrisMessageTransferDefuse.java @@ -0,0 +1,56 @@ +package iris.client_bff.iris_messages.eps; + +import iris.client_bff.core.utils.ValidationHelper; +import iris.client_bff.iris_messages.IrisMessage; +import iris.client_bff.iris_messages.IrisMessageFile; +import iris.client_bff.iris_messages.IrisMessageHdContact; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +import static iris.client_bff.ui.messages.ErrorMessages.INVALID_INPUT_STRING; + +@Service +@RequiredArgsConstructor +public class IrisMessageTransferDefuse { + + private final ValidationHelper validationHelper; + + IrisMessageTransferDto defuse(IrisMessageTransferDto message) { + return IrisMessageTransferDto.builder() + .hdAuthor(this.defuse(message.getHdAuthor(), "author")) + .hdRecipient(this.defuse(message.getHdRecipient(), "recipient")) + .subject(this.defuse(message.getSubject(), "subject", IrisMessage.SUBJECT_MAX_LENGTH)) + .body(this.defuse(message.getBody(), "body", IrisMessage.BODY_MAX_LENGTH)) + .fileAttachments(this.defuse(message.getFileAttachments())) + .build(); + } + + private IrisMessageTransferDto.HdContact defuse(IrisMessageTransferDto.HdContact contact, String field) { + return new IrisMessageTransferDto.HdContact() + .setId(this.defuse(contact.getId(), field + ".id", IrisMessageHdContact.ID_MAX_LENGTH)) + .setName(this.defuse(contact.getName(), field + ".id", IrisMessageHdContact.NAME_MAX_LENGTH)); + } + + private List defuse(List fileAttachments) { + return fileAttachments.stream().map(this::defuse).collect(Collectors.toList()); + } + + private IrisMessageTransferDto.FileAttachment defuse(IrisMessageTransferDto.FileAttachment fileAttachment) { + return new IrisMessageTransferDto.FileAttachment() + .setName(this.defuse(fileAttachment.getName(), "fileAttachment.name", IrisMessageFile.NAME_MAX_LENGTH)) + .setContent(fileAttachment.getContent()); + } + + private String defuse(String input, String field, int maxLength) { + if (input == null) return null; + if (this.validationHelper.isPossibleAttack(input, field, true)) { + return INVALID_INPUT_STRING; + } + return StringUtils.truncate(input, maxLength); + } + +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/eps/IrisMessageTransferDto.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/eps/IrisMessageTransferDto.java new file mode 100644 index 000000000..dd5b3a63a --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/eps/IrisMessageTransferDto.java @@ -0,0 +1,88 @@ +package iris.client_bff.iris_messages.eps; + +import iris.client_bff.iris_messages.IrisMessage; +import iris.client_bff.iris_messages.IrisMessageFile; +import iris.client_bff.iris_messages.IrisMessageHdContact; +import lombok.*; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.util.Base64; +import java.util.List; +import java.util.stream.Collectors; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IrisMessageTransferDto { + + // we send the name & id as we do not know if the author`s health department is accessible by the recipient + @Valid + private HdContact hdAuthor; + + // the recipient is used to check if it is identical with the endpoint identifier of the health department, the message is sent to + @Valid + private HdContact hdRecipient; + + @NotBlank + @Size(max = IrisMessage.SUBJECT_MAX_LENGTH) + private String subject; + + @NotBlank + @Size(max = IrisMessage.BODY_MAX_LENGTH) + private String body; + + @Valid + private List fileAttachments; + + public static IrisMessageTransferDto fromEntity(IrisMessage message) { + return IrisMessageTransferDto.builder() + .hdAuthor(HdContact.fromEntity(message.getHdAuthor())) + .hdRecipient(HdContact.fromEntity(message.getHdRecipient())) + .subject(message.getSubject()) + .body(message.getBody()) + .fileAttachments(FileAttachment.fromEntity(message.getFileAttachments())) + .build(); + } + + @Data + public static class HdContact { + + @NotBlank + @Size(max = IrisMessageHdContact.ID_MAX_LENGTH) + private String id; + + @NotBlank + @Size(max = IrisMessageHdContact.NAME_MAX_LENGTH) + private String name; + + public static HdContact fromEntity(IrisMessageHdContact contact) { + return new HdContact() + .setId(contact.getId()) + .setName(contact.getName()); + } + } + + @Data + public static class FileAttachment { + + @NotBlank + @Size(max = IrisMessageFile.NAME_MAX_LENGTH) + private String name; + + private String content; + + public static List fromEntity(List files) { + return files.stream().map(FileAttachment::fromEntity).collect(Collectors.toList()); + } + + public static FileAttachment fromEntity(IrisMessageFile file) { + return new FileAttachment() + .setName(file.getName()) + .setContent(Base64.getEncoder().encodeToString(file.getContent())); + } + } + +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/validation/FileTypeConstraint.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/validation/FileTypeConstraint.java new file mode 100644 index 000000000..2734e6edb --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/validation/FileTypeConstraint.java @@ -0,0 +1,22 @@ +package iris.client_bff.iris_messages.validation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.validation.Constraint; +import javax.validation.Payload; + +@Documented +@Constraint(validatedBy = FileTypeValidator.class) +@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.TYPE_USE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface FileTypeConstraint { + String message() default "{iris_message.invalid_file_type}"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/validation/FileTypeValidator.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/validation/FileTypeValidator.java new file mode 100644 index 000000000..f03904c14 --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/validation/FileTypeValidator.java @@ -0,0 +1,42 @@ +package iris.client_bff.iris_messages.validation; + +import iris.client_bff.iris_messages.web.IrisMessageInsertFileDto; +import org.apache.tika.Tika; +import org.springframework.http.MediaType; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.Base64; +import java.util.Objects; + +public class FileTypeValidator implements ConstraintValidator { + + //@todo: move ALLOWED_TYPES to a better place + public static final String[] ALLOWED_TYPES = {"image/*"}; + + @Override + public void initialize(FileTypeConstraint constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(IrisMessageInsertFileDto file, ConstraintValidatorContext ctx) { + if (file == null) return true; + try { + Tika tika = new Tika(); + String type = tika.detect(Base64.getDecoder().decode(file.getContent())); + MediaType mediaType = MediaType.valueOf(type); + for (String allowedType : ALLOWED_TYPES) { + String[] typeMap = allowedType.split("/"); + if (typeMap.length != 2) return false; + if (mediaType.getType().equals(typeMap[0])) { + if (Objects.equals(typeMap[1], "*")) return true; + if (mediaType.getSubtype().equals(typeMap[1])) return true; + } + } + } catch (Throwable e) { + return false; + } + return false; + } +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/validation/IrisMessageFileConstraint.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/validation/IrisMessageFileConstraint.java new file mode 100644 index 000000000..83ee6106f --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/validation/IrisMessageFileConstraint.java @@ -0,0 +1,22 @@ +package iris.client_bff.iris_messages.validation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.validation.Constraint; +import javax.validation.Payload; + +@Documented +@Constraint(validatedBy = IrisMessageFileValidator.class) +@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.TYPE_USE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface IrisMessageFileConstraint { + String message() default "{iris_message.invalid_file}"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/validation/IrisMessageFileValidator.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/validation/IrisMessageFileValidator.java new file mode 100644 index 000000000..d7ea7ee2a --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/validation/IrisMessageFileValidator.java @@ -0,0 +1,36 @@ +package iris.client_bff.iris_messages.validation; + +import iris.client_bff.core.utils.ValidationHelper; +import iris.client_bff.iris_messages.web.IrisMessageInsertFileDto; +import lombok.RequiredArgsConstructor; +import org.apache.tika.Tika; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.Base64; + +@Component +@RequiredArgsConstructor +public class IrisMessageFileValidator implements ConstraintValidator { + + private final ValidationHelper validationHelper; + + @Override + public boolean isValid(IrisMessageInsertFileDto file, ConstraintValidatorContext ctx) { + if (file == null) return true; + try { + Tika tika = new Tika(); + String type = tika.detect(Base64.getDecoder().decode(file.getContent())); + MediaType.valueOf(type); + } catch (Throwable e) { + return false; + } + if (validationHelper.isPossibleAttack(file.getName(), "fileName", false)) { + return false; + } + return file.getName() != null; + } + +} \ No newline at end of file diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageController.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageController.java new file mode 100644 index 000000000..05d85423a --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageController.java @@ -0,0 +1,218 @@ +package iris.client_bff.iris_messages.web; + +import iris.client_bff.core.utils.ValidationHelper; +import iris.client_bff.iris_messages.IrisMessage; +import iris.client_bff.iris_messages.IrisMessage.IrisMessageIdentifier; +import iris.client_bff.iris_messages.IrisMessageBuilder; +import iris.client_bff.iris_messages.IrisMessageException; +import iris.client_bff.iris_messages.IrisMessageFile; +import iris.client_bff.iris_messages.IrisMessageFolder; +import iris.client_bff.iris_messages.IrisMessageFolder.IrisMessageFolderIdentifier; +import iris.client_bff.iris_messages.IrisMessageHdContact; +import iris.client_bff.iris_messages.IrisMessageService; +import iris.client_bff.iris_messages.validation.FileTypeValidator; +import iris.client_bff.ui.messages.ErrorMessages; +import lombok.AllArgsConstructor; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.validation.Valid; +import javax.validation.constraints.Size; + +import org.apache.tika.Tika; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +@RestController +@AllArgsConstructor +@Validated +@RequestMapping("/iris-messages") +public class IrisMessageController { + + private static final String FIELD_SEARCH = "search"; + + private static final String FIELD_HD_RECIPIENT = "hdRecipient"; + private static final String FIELD_SUBJECT = "subject"; + private static final String FIELD_BODY = "body"; + private static final String FIELD_FILE_ATTACHMENT = "fileAttachment"; + + private IrisMessageService irisMessageService; + private final IrisMessageBuilder irisMessageBuilder; + private final ValidationHelper validationHelper; + + @GetMapping() + public Page getMessages( + @RequestParam() IrisMessageFolderIdentifier folder, + @RequestParam(required = false) String search, + Pageable pageable) { + this.validateField(search, FIELD_SEARCH); + return this.irisMessageService.search(folder, search, pageable).map(IrisMessageListItemDto::fromEntity); + } + + @PostMapping() + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity createAndSendMessage(@Valid @RequestBody IrisMessageInsertDto irisMessageInsert, + BindingResult bindingResult) { + this.validateConstraints(bindingResult); + this.validateIrisMessageInsert(irisMessageInsert); + try { + IrisMessage message = irisMessageBuilder.build(irisMessageInsert); + IrisMessage sentMessage = irisMessageService.sendMessage(message); + URI location = ServletUriComponentsBuilder + .fromCurrentRequest() + .path("/{id}") + .buildAndExpand(sentMessage.getId()) + .toUri(); + return ResponseEntity.created(location).build(); + } catch (Throwable e) { + String errorMessage = e instanceof IrisMessageException ime + ? ime.getErrorMessage() + : "iris_message.submission_error"; + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, errorMessage); + } + } + + private void validateIrisMessageInsert(IrisMessageInsertDto irisMessageInsert) { + if (irisMessageInsert == null) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "iris_message.submission_error"); + } + this.validateField(irisMessageInsert.getHdRecipient(), FIELD_HD_RECIPIENT); + this.validateField(irisMessageInsert.getSubject(), FIELD_SUBJECT); + this.validateField(irisMessageInsert.getBody(), FIELD_BODY); + + if (irisMessageInsert.getFileAttachments() != null) { + for ( IrisMessageInsertFileDto file : irisMessageInsert.getFileAttachments() ) { + this.validateField(file.getName(), FIELD_FILE_ATTACHMENT); + } + } + } + + @GetMapping("/allowed-file-types") + public ResponseEntity getAllowedFileTypes() { + return ResponseEntity.ok(FileTypeValidator.ALLOWED_TYPES); + } + + @GetMapping("/{messageId}") + public ResponseEntity getMessageDetails(@PathVariable IrisMessageIdentifier messageId) { + Optional irisMessage = this.irisMessageService.findById(messageId); + if (irisMessage.isPresent()) { + IrisMessageDetailsDto messageDetailsDto = IrisMessageDetailsDto.fromEntity(irisMessage.get()); + return ResponseEntity.ok(messageDetailsDto); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + @PatchMapping("/{messageId}") + public ResponseEntity updateMessage( + @PathVariable IrisMessageIdentifier messageId, + @RequestBody @Valid IrisMessageUpdateDto irisMessageUpdate, + BindingResult bindingResult) { + this.validateConstraints(bindingResult); + this.validateIrisMessageUpdate(irisMessageUpdate); + Optional optionalMessage = this.irisMessageService.findById(messageId); + if (optionalMessage.isPresent()) { + IrisMessage message = optionalMessage.get(); + message.setIsRead(irisMessageUpdate.getIsRead()); + IrisMessage updatedMessage = this.irisMessageService.saveMessage(message); + return ResponseEntity.ok(IrisMessageDetailsDto.fromEntity(updatedMessage)); + } + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + private void validateIrisMessageUpdate(IrisMessageUpdateDto irisMessageUpdate) { + if (irisMessageUpdate == null || irisMessageUpdate.getIsRead() == null) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ErrorMessages.INVALID_INPUT); + } + } + + @GetMapping("/folders") + public ResponseEntity> getMessageFolders() { + List irisMessageFolders = irisMessageService.getFolders(); + // there should always be at least one inbox and one outbox folder. No folders at all = error + if (irisMessageFolders.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + return ResponseEntity.ok(IrisMessageFolderDto.fromEntity(irisMessageFolders)); + } + + @GetMapping("/files/{id}/download") + public ResponseEntity downloadMessageFile(@PathVariable IrisMessageFile.IrisMessageFileIdentifier id) { + Optional file = this.irisMessageService.findFileById(id); + if (file.isPresent()) { + try { + IrisMessageFile messageFile = file.get(); + int contentLength = 0; + MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM; + if (messageFile.getContent() != null) { + contentLength = messageFile.getContent().length; + Tika tika = new Tika(); + String contentType = tika.detect(messageFile.getContent(), messageFile.getName()); + mediaType = MediaType.valueOf(contentType); + } + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + messageFile.getName() + "\"") + .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION) + .contentLength(contentLength) + .contentType(mediaType) + .body(messageFile.getContent()); + } catch(Throwable e) { + throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "iris_message.invalid_file"); + } + } + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + @GetMapping("/hd-contacts") + public ResponseEntity> getMessageHdContacts( + @Valid @Size(max = 100) @RequestParam(required = false) String search, + @RequestParam(defaultValue = "false") boolean includeOwn) { + validateField(search, FIELD_SEARCH); + try { + ArrayList irisMessageContacts = new ArrayList<>(irisMessageService.getHdContacts(search)); + if (includeOwn) { + irisMessageContacts.add(irisMessageService.getOwnHdContact()); + } + return ResponseEntity.ok(irisMessageContacts); + } catch (Throwable e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "iris_message.missing_hd_contacts"); + } + } + + @GetMapping("/count/unread") + public ResponseEntity getUnreadMessageCount( + @RequestParam(required = false) IrisMessageFolderIdentifier folder) { + if (folder == null) { + return ResponseEntity.ok(irisMessageService.getCountUnread()); + } + return ResponseEntity.ok(irisMessageService.getCountUnreadByFolderId(folder)); + } + + private void validateConstraints(BindingResult bindingResult) { + if (bindingResult.hasErrors()) { + String message = ErrorMessages.INVALID_INPUT + ": " + bindingResult.getFieldErrors().stream() + .map(fieldError -> String.format("%s: %s", fieldError.getField(), fieldError.getDefaultMessage())) + .collect(Collectors.joining(", ")); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message); + } + } + + private void validateField(String value, String field) { + if (validationHelper.isPossibleAttack(value, field, false)) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ErrorMessages.INVALID_INPUT); + } + } +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageDetailsDto.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageDetailsDto.java new file mode 100644 index 000000000..afa20146d --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageDetailsDto.java @@ -0,0 +1,65 @@ +package iris.client_bff.iris_messages.web; + +import iris.client_bff.iris_messages.IrisMessage; +import iris.client_bff.iris_messages.IrisMessageFile; +import iris.client_bff.iris_messages.IrisMessageHdContact; +import lombok.*; +import org.apache.tika.Tika; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Value +public class IrisMessageDetailsDto { + + private String id; + private String subject; + private String body; + private IrisMessageHdContact hdAuthor; + private IrisMessageHdContact hdRecipient; + private Instant createdAt; + private Boolean isRead; + private List fileAttachments; + private Boolean hasFileAttachments; + + public static IrisMessageDetailsDto fromEntity(IrisMessage message) { + List fileAttachments = FileAttachment.fromEntity(message.getFileAttachments()); + return new IrisMessageDetailsDto( + message.getId().toString(), + message.getSubject(), + message.getBody(), + message.getHdAuthor(), + message.getHdRecipient(), + message.getMetadata().getCreated(), + message.getIsRead(), + fileAttachments, + fileAttachments.size() > 0 + ); + } + + @Value + public static class FileAttachment { + + private String id; + private String name; + private String type; + + public static List fromEntity(List files) { + if (files == null) return new ArrayList<>(); + return files.stream().map(FileAttachment::fromEntity).collect(Collectors.toList()); + } + + public static FileAttachment fromEntity(IrisMessageFile file) { + Tika tika = new Tika(); + String type = tika.detect(file.getContent(), file.getName()); + return new FileAttachment( + file.getId().toString(), + file.getName(), + type + ); + } + } + +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageFolderDto.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageFolderDto.java new file mode 100644 index 000000000..6e0a59a21 --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageFolderDto.java @@ -0,0 +1,60 @@ +package iris.client_bff.iris_messages.web; + +import iris.client_bff.iris_messages.IrisMessageContext; +import iris.client_bff.iris_messages.IrisMessageFolder; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class IrisMessageFolderDto { + + private String id; + private String name; + private IrisMessageContext context; + private List items; + private Boolean isDefault; + + private static IrisMessageFolderDto findParentFolderDto(List folderDtoList, IrisMessageFolder folder) { + if (folder.getParentFolder() == null) { + return null; + } + for ( IrisMessageFolderDto folderDto : folderDtoList ) { + if(folderDto.getId().equals(folder.getParentFolder().toString())) { + return folderDto; + } + if(!folderDto.items.isEmpty()) { + IrisMessageFolderDto parentFolderDto = IrisMessageFolderDto.findParentFolderDto(folderDto.items, folder); + if (parentFolderDto != null) { + return parentFolderDto; + } + } + } + return null; + } + + public static IrisMessageFolderDto fromEntity(IrisMessageFolder folder) { + return new IrisMessageFolderDto() + .setId(folder.getId().toString()) + .setName(folder.getName()) + .setContext(folder.getContext()) + .setIsDefault(folder.getIsDefault()) + .setItems(new ArrayList<>()); + } + + public static List fromEntity(List folderList) { + List folderDtoList = new ArrayList<>(); + for ( IrisMessageFolder folder : folderList ) { + IrisMessageFolderDto folderDto = IrisMessageFolderDto.fromEntity(folder); + IrisMessageFolderDto parentFolderDto = IrisMessageFolderDto.findParentFolderDto(folderDtoList, folder); + if (parentFolderDto != null) { + parentFolderDto.items.add(folderDto); + } else { + folderDtoList.add( folderDto ); + } + } + return folderDtoList; + } + +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageInsertDto.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageInsertDto.java new file mode 100644 index 000000000..2cf0f80aa --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageInsertDto.java @@ -0,0 +1,31 @@ +package iris.client_bff.iris_messages.web; + +import iris.client_bff.iris_messages.IrisMessage; +import iris.client_bff.iris_messages.IrisMessageHdContact; +import iris.client_bff.iris_messages.validation.FileTypeConstraint; +import iris.client_bff.iris_messages.validation.IrisMessageFileConstraint; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.util.List; + +@Data +public class IrisMessageInsertDto { + + @NotBlank + @Size(max = IrisMessageHdContact.ID_MAX_LENGTH) + private String hdRecipient; + + @NotBlank + @Size(max = IrisMessage.SUBJECT_MAX_LENGTH) + private String subject; + + @NotBlank + @Size(max = IrisMessage.BODY_MAX_LENGTH) + private String body; + + @Valid + private List<@IrisMessageFileConstraint @FileTypeConstraint IrisMessageInsertFileDto> fileAttachments; +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageInsertFileDto.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageInsertFileDto.java new file mode 100644 index 000000000..6323c3a93 --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageInsertFileDto.java @@ -0,0 +1,18 @@ +package iris.client_bff.iris_messages.web; + +import iris.client_bff.iris_messages.IrisMessageFile; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@Data +public class IrisMessageInsertFileDto { + + @NotBlank + @Size(max = IrisMessageFile.NAME_MAX_LENGTH) + private String name; + + @NotBlank + private String content; +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageListItemDto.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageListItemDto.java new file mode 100644 index 000000000..4a522f97a --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageListItemDto.java @@ -0,0 +1,32 @@ +package iris.client_bff.iris_messages.web; + +import iris.client_bff.iris_messages.IrisMessage; +import iris.client_bff.iris_messages.IrisMessageHdContact; +import lombok.Value; + +import java.time.Instant; + +@Value +public class IrisMessageListItemDto { + + private String id; + private String subject; + private IrisMessageHdContact hdAuthor; + private IrisMessageHdContact hdRecipient; + private Instant createdAt; + private Boolean isRead; + private Boolean hasFileAttachments; + + public static IrisMessageListItemDto fromEntity(IrisMessage message) { + Boolean hasFileAttachments = message.getFileAttachments() != null && message.getFileAttachments().size() > 0; + return new IrisMessageListItemDto( + message.getId().toString(), + message.getSubject(), + message.getHdAuthor(), + message.getHdRecipient(), + message.getMetadata().getCreated(), + message.getIsRead(), + hasFileAttachments + ); + } +} diff --git a/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageUpdateDto.java b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageUpdateDto.java new file mode 100644 index 000000000..50c0a07e5 --- /dev/null +++ b/iris-client-bff/src/main/java/iris/client_bff/iris_messages/web/IrisMessageUpdateDto.java @@ -0,0 +1,17 @@ +package iris.client_bff.iris_messages.web; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class IrisMessageUpdateDto { + + @NotNull + private Boolean isRead; + +} diff --git a/iris-client-bff/src/main/resources/db/migration/V1009__add_iris_messages.sql b/iris-client-bff/src/main/resources/db/migration/V1009__add_iris_messages.sql new file mode 100644 index 000000000..e8e410f18 --- /dev/null +++ b/iris-client-bff/src/main/resources/db/migration/V1009__add_iris_messages.sql @@ -0,0 +1,38 @@ +CREATE TABLE iris_message_folder ( + id uuid NOT NULL, + name varchar(255) NOT NULL, + parent_folder uuid NULL, + context varchar(50) NOT NULL, + created timestamp NOT NULL, + last_modified timestamp NOT NULL, + created_by uuid NULL, + last_modified_by uuid NULL, + CONSTRAINT iris_message_folder_pkey PRIMARY KEY (id), + FOREIGN KEY (created_by) REFERENCES user_accounts(user_id), + FOREIGN KEY (last_modified_by) REFERENCES user_accounts(user_id) +); + +CREATE TABLE iris_message_folder_default ( + id uuid NOT NULL, + CONSTRAINT iris_message_folder_default_pkey PRIMARY KEY (id) +); + +CREATE TABLE iris_message ( + id uuid NOT NULL, + folder_id uuid NOT NULL, + subject varchar(500) NOT NULL, + body varchar(6000) NOT NULL, + hd_author_id varchar(255) NOT NULL, + hd_author_name varchar(255) NOT NULL, + hd_recipient_id varchar(255) NOT NULL, + hd_recipient_name varchar(255) NOT NULL, + is_read bool NULL, + created timestamp NOT NULL, + last_modified timestamp NOT NULL, + created_by uuid NULL, + last_modified_by uuid NULL, + CONSTRAINT iris_message_pkey PRIMARY KEY (id), + CONSTRAINT iris_message_folder_fk FOREIGN KEY (folder_id) REFERENCES iris_message_folder(id), + FOREIGN KEY (created_by) REFERENCES user_accounts(user_id), + FOREIGN KEY (last_modified_by) REFERENCES user_accounts(user_id) +); diff --git a/iris-client-bff/src/main/resources/db/migration/V1011__add_iris_message_files.sql b/iris-client-bff/src/main/resources/db/migration/V1011__add_iris_message_files.sql new file mode 100644 index 000000000..2851cb550 --- /dev/null +++ b/iris-client-bff/src/main/resources/db/migration/V1011__add_iris_message_files.sql @@ -0,0 +1,14 @@ +CREATE TABLE iris_message_file ( + id uuid NOT NULL, + message_id uuid NOT NULL, + name varchar(255) NOT NULL, + content bytea NULL, + created timestamp NOT NULL, + last_modified timestamp NOT NULL, + created_by uuid NULL, + last_modified_by uuid NULL, + CONSTRAINT iris_message_file_pkey PRIMARY KEY (id), + CONSTRAINT iris_message_file_message_fk FOREIGN KEY (message_id) REFERENCES iris_message(id), + FOREIGN KEY (created_by) REFERENCES user_accounts(user_id), + FOREIGN KEY (last_modified_by) REFERENCES user_accounts(user_id) +); diff --git a/iris-client-bff/src/main/resources/db/migration_mssql/V1009__add_iris_messages.sql b/iris-client-bff/src/main/resources/db/migration_mssql/V1009__add_iris_messages.sql new file mode 100644 index 000000000..3cecf61c3 --- /dev/null +++ b/iris-client-bff/src/main/resources/db/migration_mssql/V1009__add_iris_messages.sql @@ -0,0 +1,38 @@ +CREATE TABLE iris_message_folder ( + id binary(255) NOT NULL, + name varchar(255) NOT NULL, + parent_folder binary(255) NULL, + context varchar(50) NOT NULL, + created datetime2 NOT NULL, + last_modified datetime2 NOT NULL, + created_by binary(255) NULL, + last_modified_by binary(255) NULL, + CONSTRAINT iris_message_folder_pkey PRIMARY KEY (id), + FOREIGN KEY (created_by) REFERENCES user_accounts(user_id), + FOREIGN KEY (last_modified_by) REFERENCES user_accounts(user_id) +); + +CREATE TABLE iris_message_folder_default ( + id binary(255) NOT NULL, + CONSTRAINT iris_message_folder_default_pkey PRIMARY KEY (id) +); + +CREATE TABLE iris_message ( + id binary(255) NOT NULL, + folder_id binary(255) NOT NULL, + subject varchar(500) NOT NULL, + body varchar(6000) NOT NULL, + hd_author_id varchar(255) NOT NULL, + hd_author_name varchar(255) NOT NULL, + hd_recipient_id varchar(255) NOT NULL, + hd_recipient_name varchar(255) NOT NULL, + is_read bit NULL, + created datetime2 NOT NULL, + last_modified datetime2 NOT NULL, + created_by binary(255) NULL, + last_modified_by binary(255) NULL, + CONSTRAINT iris_message_pkey PRIMARY KEY (id), + CONSTRAINT iris_message_folder_fk FOREIGN KEY (folder_id) REFERENCES iris_message_folder(id), + FOREIGN KEY (created_by) REFERENCES user_accounts(user_id), + FOREIGN KEY (last_modified_by) REFERENCES user_accounts(user_id) +); diff --git a/iris-client-bff/src/main/resources/db/migration_mssql/V1011__add_iris_message_files.sql b/iris-client-bff/src/main/resources/db/migration_mssql/V1011__add_iris_message_files.sql new file mode 100644 index 000000000..e24f0785a --- /dev/null +++ b/iris-client-bff/src/main/resources/db/migration_mssql/V1011__add_iris_message_files.sql @@ -0,0 +1,15 @@ + +CREATE TABLE iris_message_file ( + id binary(255) NOT NULL, + message_id binary(255) NOT NULL, + name varchar(255) NOT NULL, + content varbinary(max) NULL, + created datetime2 NOT NULL, + last_modified datetime2 NOT NULL, + created_by binary(255) NULL, + last_modified_by binary(255) NULL, + CONSTRAINT iris_message_file_pkey PRIMARY KEY (id), + CONSTRAINT iris_message_file_message_fk FOREIGN KEY (message_id) REFERENCES iris_message(id), + FOREIGN KEY (created_by) REFERENCES user_accounts(user_id), + FOREIGN KEY (last_modified_by) REFERENCES user_accounts(user_id) +); diff --git a/iris-client-bff/src/main/resources/db/migration_mysql/V1009__add_iris_messages.sql b/iris-client-bff/src/main/resources/db/migration_mysql/V1009__add_iris_messages.sql new file mode 100644 index 000000000..a8eea81f9 --- /dev/null +++ b/iris-client-bff/src/main/resources/db/migration_mysql/V1009__add_iris_messages.sql @@ -0,0 +1,38 @@ +CREATE TABLE iris_message_folder ( + id binary(16) NOT NULL, + name varchar(255) NOT NULL, + parent_folder binary(16) NULL, + context varchar(50) NOT NULL, + created datetime NOT NULL, + last_modified datetime NOT NULL, + created_by binary(16) NULL, + last_modified_by binary(16) NULL, + CONSTRAINT iris_message_folder_pkey PRIMARY KEY (id), + FOREIGN KEY (created_by) REFERENCES user_accounts(user_id), + FOREIGN KEY (last_modified_by) REFERENCES user_accounts(user_id) +); + +CREATE TABLE iris_message_folder_default ( + id binary(16) NOT NULL, + CONSTRAINT iris_message_folder_default_pkey PRIMARY KEY (id) +); + +CREATE TABLE iris_message ( + id binary(16) NOT NULL, + folder_id binary(16) NOT NULL, + subject varchar(500) NOT NULL, + body varchar(6000) NOT NULL, + hd_author_id varchar(255) NOT NULL, + hd_author_name varchar(255) NOT NULL, + hd_recipient_id varchar(255) NOT NULL, + hd_recipient_name varchar(255) NOT NULL, + is_read bool NULL, + created datetime NOT NULL, + last_modified datetime NOT NULL, + created_by binary(16) NULL, + last_modified_by binary(16) NULL, + CONSTRAINT iris_message_pkey PRIMARY KEY (id), + CONSTRAINT iris_message_folder_fk FOREIGN KEY (folder_id) REFERENCES iris_message_folder(id), + FOREIGN KEY (created_by) REFERENCES user_accounts(user_id), + FOREIGN KEY (last_modified_by) REFERENCES user_accounts(user_id) +); diff --git a/iris-client-bff/src/main/resources/db/migration_mysql/V1011__add_iris_message_files.sql b/iris-client-bff/src/main/resources/db/migration_mysql/V1011__add_iris_message_files.sql new file mode 100644 index 000000000..c70e1202a --- /dev/null +++ b/iris-client-bff/src/main/resources/db/migration_mysql/V1011__add_iris_message_files.sql @@ -0,0 +1,14 @@ +CREATE TABLE iris_message_file ( + id binary(16) NOT NULL, + message_id binary(16) NOT NULL, + name varchar(255) NOT NULL, + content mediumblob NULL, + created datetime NOT NULL, + last_modified datetime NOT NULL, + created_by binary(16) NULL, + last_modified_by binary(16) NULL, + CONSTRAINT iris_message_file_pkey PRIMARY KEY (id), + CONSTRAINT iris_message_file_message_fk FOREIGN KEY (message_id) REFERENCES iris_message(id), + FOREIGN KEY (created_by) REFERENCES user_accounts(user_id), + FOREIGN KEY (last_modified_by) REFERENCES user_accounts(user_id) +); diff --git a/iris-client-bff/src/main/resources/messages.properties b/iris-client-bff/src/main/resources/messages.properties index 708ce7b8d..f13f20b0b 100644 --- a/iris-client-bff/src/main/resources/messages.properties +++ b/iris-client-bff/src/main/resources/messages.properties @@ -17,3 +17,12 @@ app.status.connection_closed_by_remote=Eine vorhandene Verbindung wurde vom Remo app.status.access_denied=Es konnte keine Verbindung hergestellt werden, da der Zielcomputer die Verbindung verweigerte. app.status.no_such_host=Der Host des App Anbieters kann nicht ermittelt werden. app.status.timeout=Es gab eine Zeitüberschreitung bei der Anfrage. + +iris_message.submission_error=Fehler: Nachricht konnte nicht gesendet werden +iris_message.invalid_id=Die Nachrichten ID ist ungültig. +iris_message.invalid_recipient=Der Empfänger der Nachricht ist ungültig. +iris_message.missing_hd_contacts=Die Gesundheitsamt-Kontakte konnten nicht geladen werden. +iris_message.invalid_folder=Der Nachrichten-Ordner ist ungültig. +iris_message.invalid_file=Der Nachrichten Anhang ist ungültig. +iris_message.invalid_file_type=Der Dateityp ist ungültig. +iris_message.max_upload_file_size=Die Maximalgröße für Dateien von {0} wurde überschritten. diff --git a/iris-client-bff/src/main/resources/messages_de.properties b/iris-client-bff/src/main/resources/messages_de.properties index 708ce7b8d..f13f20b0b 100644 --- a/iris-client-bff/src/main/resources/messages_de.properties +++ b/iris-client-bff/src/main/resources/messages_de.properties @@ -17,3 +17,12 @@ app.status.connection_closed_by_remote=Eine vorhandene Verbindung wurde vom Remo app.status.access_denied=Es konnte keine Verbindung hergestellt werden, da der Zielcomputer die Verbindung verweigerte. app.status.no_such_host=Der Host des App Anbieters kann nicht ermittelt werden. app.status.timeout=Es gab eine Zeitüberschreitung bei der Anfrage. + +iris_message.submission_error=Fehler: Nachricht konnte nicht gesendet werden +iris_message.invalid_id=Die Nachrichten ID ist ungültig. +iris_message.invalid_recipient=Der Empfänger der Nachricht ist ungültig. +iris_message.missing_hd_contacts=Die Gesundheitsamt-Kontakte konnten nicht geladen werden. +iris_message.invalid_folder=Der Nachrichten-Ordner ist ungültig. +iris_message.invalid_file=Der Nachrichten Anhang ist ungültig. +iris_message.invalid_file_type=Der Dateityp ist ungültig. +iris_message.max_upload_file_size=Die Maximalgröße für Dateien von {0} wurde überschritten. diff --git a/iris-client-bff/src/main/resources/messages_en.properties b/iris-client-bff/src/main/resources/messages_en.properties index e28b429de..fa936f6d3 100644 --- a/iris-client-bff/src/main/resources/messages_en.properties +++ b/iris-client-bff/src/main/resources/messages_en.properties @@ -17,3 +17,12 @@ app.status.connection_closed_by_remote=An existing connection has been closed by app.status.access_denied=A connection could not be established because the target computer refused to connect. app.status.no_such_host=The host of the app provider cannot be determined. app.status.timeout=The request timed out. + +iris_message.submission_error=Error: Message could not be sent +iris_message.invalid_id=The message ID is invalid. +iris_message.invalid_recipient=The recipient of the message is invalid. +iris_message.missing_hd_contacts=The health department contacts could not be loaded. +iris_message.invalid_folder=The message folder is invalid. +iris_message.invalid_file=The message attachment is invalid. +iris_message.invalid_file_type=The file type is invalid. +iris_message.max_upload_file_size=The maximum file size of {0} has been exceeded. diff --git a/iris-client-bff/src/test/java/iris/client_bff/core/mail/EmailSenderIntegrationTests.java b/iris-client-bff/src/test/java/iris/client_bff/core/mail/EmailSenderIntegrationTests.java index 6fdcb30ef..321875d81 100644 --- a/iris-client-bff/src/test/java/iris/client_bff/core/mail/EmailSenderIntegrationTests.java +++ b/iris-client-bff/src/test/java/iris/client_bff/core/mail/EmailSenderIntegrationTests.java @@ -112,7 +112,7 @@ Try sendTestEmailEn() { Try sendTestEmailDe() { - var locale = Locale.GERMAN; + var locale = new Locale("de", "DE", "test"); var subject = messages.getMessage("TestMail.subject", locale); var email = new TestEmail(subject, TestKeys.TEST_MAIL_FTL, getParameters(), locale); @@ -122,7 +122,7 @@ Try sendTestEmailDe() { Try sendTestHtmlEmailDe() { - var locale = Locale.GERMAN; + var locale = new Locale("de", "DE", "test"); var subject = messages.getMessage("TestMail.subject", locale); var email = new TestEmail(subject, TestKeys.TEST_HTML_MAIL_FTLH, getParameters(), locale); diff --git a/iris-client-bff/src/test/java/iris/client_bff/dbms/DatabasesystemIT.java b/iris-client-bff/src/test/java/iris/client_bff/dbms/DatabasesystemIT.java index 5a709913d..3464afbf3 100644 --- a/iris-client-bff/src/test/java/iris/client_bff/dbms/DatabasesystemIT.java +++ b/iris-client-bff/src/test/java/iris/client_bff/dbms/DatabasesystemIT.java @@ -12,8 +12,14 @@ import iris.client_bff.events.EventDataRequestService; import iris.client_bff.events.EventDataRequestsDataInitializer; import iris.client_bff.events.EventDataSubmissionRepository; +import iris.client_bff.iris_messages.IrisMessageContext; +import iris.client_bff.iris_messages.IrisMessageFolder; +import iris.client_bff.iris_messages.IrisMessageFolderRepository; +import iris.client_bff.iris_messages.IrisMessageService; import java.time.Instant; +import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -36,6 +42,10 @@ abstract class DatabasesystemIT { private EventDataRequestService eventReqService; @Autowired private CaseDataRequestService caseReqService; + @Autowired + private IrisMessageService irisMessageService; + @Autowired + private IrisMessageFolderRepository irisMessageFolderRepository; @Test void eventRequests() { @@ -116,4 +126,31 @@ void caseSubmissions() { assertThat(caseSubmissions.findAllByRequest(CaseDataRequestDataInitializer.DATA_REQUEST_1).toList()).hasSize(1); } + @Test + void irisMessages() { + + List inboxRootFolders = irisMessageFolderRepository.findAll(); + assertThat(inboxRootFolders).hasSize(3); + + Optional inboxRootFolder = irisMessageFolderRepository.findFirstByContextAndParentFolderIsNull(IrisMessageContext.INBOX); + assertThat(inboxRootFolder.isPresent()).isTrue(); + assertThat(irisMessageService.search(inboxRootFolder.get().getId(), null, null).toList()).hasSize(2); + + Optional outboxRootFolder = irisMessageFolderRepository.findFirstByContextAndParentFolderIsNull(IrisMessageContext.OUTBOX); + assertThat(outboxRootFolder.isPresent()).isTrue(); + assertThat(irisMessageService.search(outboxRootFolder.get().getId(), null, null).toList()).hasSize(1); + + List inboxNestedFolders = irisMessageFolderRepository.findAllByParentFolder(inboxRootFolder.get().getId()); + assertThat(inboxNestedFolders).hasSize(1); + + assertThat(irisMessageService.search(inboxNestedFolders.get(0).getId(), null, null).toList()).hasSize(1); + + assertThat(irisMessageService.getCountUnread()).isEqualTo(3); + + assertThat(irisMessageService.getCountUnreadByFolderId(inboxRootFolder.get().getId())).isEqualTo(2); + assertThat(irisMessageService.getCountUnreadByFolderId(outboxRootFolder.get().getId())).isEqualTo(0); + assertThat(irisMessageService.getCountUnreadByFolderId(inboxNestedFolders.get(0).getId())).isEqualTo(1); + + } + } diff --git a/iris-client-bff/src/test/java/iris/client_bff/iris_messages/IrisMessageBuilderTest.java b/iris-client-bff/src/test/java/iris/client_bff/iris_messages/IrisMessageBuilderTest.java new file mode 100644 index 000000000..acdcf2a6f --- /dev/null +++ b/iris-client-bff/src/test/java/iris/client_bff/iris_messages/IrisMessageBuilderTest.java @@ -0,0 +1,85 @@ +package iris.client_bff.iris_messages; + +import iris.client_bff.iris_messages.eps.EPSIrisMessageClient; +import iris.client_bff.iris_messages.eps.IrisMessageTransferDto; +import iris.client_bff.iris_messages.web.IrisMessageInsertDto; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.support.MessageSourceAccessor; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class IrisMessageBuilderTest { + + IrisMessageTestData testData; + + @Mock + IrisMessageFolderRepository folderRepository; + + @Mock + EPSIrisMessageClient irisMessageClient; + + @Mock + MessageSourceAccessor messages; + + IrisMessageBuilder builder; + + @BeforeEach + void setUp() { + this.testData = new IrisMessageTestData(); + this.builder = new IrisMessageBuilder(this.folderRepository, this.irisMessageClient, this.messages); + } + + @Test + void buildTransfer() { + + IrisMessage message = this.testData.MOCK_INBOX_MESSAGE; + IrisMessageTransferDto messageTransfer = IrisMessageTransferDto.fromEntity(message); + + when(this.folderRepository.findFirstByContextAndParentFolderIsNull(any())).thenReturn(Optional.of(this.testData.MOCK_INBOX_FOLDER)); + + when(this.irisMessageClient.getOwnIrisMessageHdContact()).thenReturn(this.testData.MOCK_CONTACT_OWN); + + var builtMessage = this.builder.build(messageTransfer); + + verify(this.folderRepository).findFirstByContextAndParentFolderIsNull(any()); + + verify(this.irisMessageClient).getOwnIrisMessageHdContact(); + + // messages should be identical except ID: toString removes the ID + assertEquals(message.toString(), builtMessage.toString()); + + } + + @Test + void buildInsert() { + + IrisMessage message = this.testData.MOCK_OUTBOX_MESSAGE; + IrisMessageInsertDto messageInsert = this.testData.getTestMessageInsert(message); + + when(this.folderRepository.findFirstByContextAndParentFolderIsNull(any())).thenReturn(Optional.of(this.testData.MOCK_OUTBOX_FOLDER)); + + when(this.irisMessageClient.getOwnIrisMessageHdContact()).thenReturn(this.testData.MOCK_CONTACT_OWN); + when(this.irisMessageClient.findIrisMessageHdContactById(any(String.class))).thenReturn(Optional.of(this.testData.MOCK_CONTACT_OTHER)); + + var builtMessage = this.builder.build(messageInsert); + + verify(this.folderRepository).findFirstByContextAndParentFolderIsNull(any()); + + verify(this.irisMessageClient).getOwnIrisMessageHdContact(); + verify(this.irisMessageClient).findIrisMessageHdContactById(any(String.class)); + + // messages should be identical except ID: toString removes the ID + assertEquals(message.toString(), builtMessage.toString()); + + } + +} diff --git a/iris-client-bff/src/test/java/iris/client_bff/iris_messages/IrisMessageDataInitializer.java b/iris-client-bff/src/test/java/iris/client_bff/iris_messages/IrisMessageDataInitializer.java new file mode 100644 index 000000000..c58d48e20 --- /dev/null +++ b/iris-client-bff/src/test/java/iris/client_bff/iris_messages/IrisMessageDataInitializer.java @@ -0,0 +1,56 @@ +package iris.client_bff.iris_messages; + +import static org.assertj.core.api.Assertions.*; + +import iris.client_bff.DataInitializer; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Optional; + +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +@Order(10) +public class IrisMessageDataInitializer implements DataInitializer { + + private final IrisMessageRepository messageRepository; + + private final IrisMessageFolderRepository folderRepository; + + private final IrisMessageTestData testData; + + @Getter + private IrisMessageFolder inboxFolder; + + @Override + public void initialize() { + + log.debug("Test data: creating iris messages …"); + + Optional folder = this.folderRepository + .findFirstByContextAndParentFolderIsNull(IrisMessageContext.INBOX); + assertThat(folder).isPresent(); + this.inboxFolder = folder.get(); + IrisMessageFolder nestedInboxFolder = this.testData.getTestMessageFolder(inboxFolder, "nested inbox"); + + this.folderRepository.save(nestedInboxFolder); + + Optional outboxFolder = this.folderRepository + .findFirstByContextAndParentFolderIsNull(IrisMessageContext.OUTBOX); + assertThat(outboxFolder).isPresent(); + + var message = this.testData.getTestInboxMessage(inboxFolder); + message.setSubject("First test inbox subject"); + this.messageRepository.save(message); + this.messageRepository.save(this.testData.getTestInboxMessage(inboxFolder)); + + this.messageRepository.save(this.testData.getTestInboxMessage(nestedInboxFolder)); + + this.messageRepository.save(this.testData.getTestOutboxMessage(outboxFolder.get())); + } +} diff --git a/iris-client-bff/src/test/java/iris/client_bff/iris_messages/IrisMessageServiceTest.java b/iris-client-bff/src/test/java/iris/client_bff/iris_messages/IrisMessageServiceTest.java new file mode 100644 index 000000000..7c736d5cf --- /dev/null +++ b/iris-client-bff/src/test/java/iris/client_bff/iris_messages/IrisMessageServiceTest.java @@ -0,0 +1,225 @@ +package iris.client_bff.iris_messages; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import iris.client_bff.core.utils.HibernateSearcher; +import iris.client_bff.hd_search.eps.EPSHdSearchClient; +import iris.client_bff.iris_messages.IrisMessage.IrisMessageIdentifier; +import iris.client_bff.iris_messages.eps.EPSIrisMessageClient; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.AdditionalAnswers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +@ExtendWith(MockitoExtension.class) +public class IrisMessageServiceTest { + + IrisMessageTestData testData; + + @Mock + IrisMessageRepository messageRepository; + + @Mock + IrisMessageFolderRepository folderRepository; + + @Mock + IrisMessageFileRepository fileRepository; + + @Mock + HibernateSearcher searcher; + + @Mock + EPSIrisMessageClient irisMessageClient; + + @Mock + EPSHdSearchClient hdSearchClient; + + IrisMessageService service; + + private final IrisMessageIdentifier ID_NOT_FOUND = IrisMessageIdentifier.of(UUID.randomUUID()); + + private final IrisMessageFile.IrisMessageFileIdentifier FILE_ID_NOT_FOUND = IrisMessageFile.IrisMessageFileIdentifier.of(UUID.randomUUID()); + + @BeforeEach + void setUp() { + this.testData = new IrisMessageTestData(); + this.service = new IrisMessageService( + this.messageRepository, + this.folderRepository, + this.fileRepository, + this.searcher, + this.irisMessageClient, + this.hdSearchClient); + } + + @Test + void findById() { + when(this.messageRepository.findById(any())).thenReturn(Optional.of(this.testData.MOCK_INBOX_MESSAGE)); + + var message = this.service.findById(this.testData.MOCK_INBOX_MESSAGE.getId()); + + verify(this.messageRepository).findById(any()); + + assertTrue(message.isPresent()); + assertEquals(message.get(), this.testData.MOCK_INBOX_MESSAGE); + } + + @Test + void findById_notFound() { + when(this.messageRepository.findById(this.ID_NOT_FOUND)) + .thenReturn(Optional.empty()); + + var message = this.service.findById(this.ID_NOT_FOUND); + + verify(this.messageRepository).findById(any()); + + assertTrue(message.isEmpty()); + } + + @Test + void search() { + + IrisMessage message = this.testData.MOCK_INBOX_MESSAGE; + + var folderId = message.getFolder().getId(); + + Page page = new PageImpl<>(List.of(message)); + + when(this.messageRepository.findAllByFolderIdOrderByIsReadAsc(eq(folderId), nullable(Pageable.class))) + .thenReturn(page); + + var messagePage = this.service.search(folderId, null, null); + + verify(this.messageRepository).findAllByFolderIdOrderByIsReadAsc(eq(folderId), nullable(Pageable.class)); + + assertEquals(1, messagePage.getContent().size()); + assertEquals(page.getContent(), messagePage.getContent()); + } + + @Test + void getCountUnreadByFolderId() { + + var folderId = this.testData.MOCK_INBOX_FOLDER.getId(); + + when(this.messageRepository.getCountUnreadByFolderId(any(IrisMessageFolder.IrisMessageFolderIdentifier.class))) + .thenReturn(3); + + var count = this.service.getCountUnreadByFolderId(folderId); + + verify(this.messageRepository).getCountUnreadByFolderId(eq(folderId)); + + assertEquals(3, count); + + } + + @Test + void getCountUnread() { + + when(this.messageRepository.countByIsReadFalseOrIsReadIsNull()).thenReturn(3); + + var count = this.service.getCountUnread(); + + verify(this.messageRepository).countByIsReadFalseOrIsReadIsNull(); + + assertEquals(3, count); + + } + + @Test + void getFolders() { + + when(this.folderRepository.findAll()) + .thenReturn(List.of(this.testData.MOCK_INBOX_FOLDER, this.testData.MOCK_OUTBOX_FOLDER)); + + var folders = this.service.getFolders(); + + verify(this.folderRepository).findAll(); + + assertEquals(2, folders.size()); + + } + + @Test + void findFileById() { + + when(this.fileRepository.findById(any())).thenReturn(Optional.of(this.testData.MOCK_MESSAGE_FILE)); + + var file = this.service.findFileById(this.testData.MOCK_MESSAGE_FILE.getId()); + + verify(this.fileRepository).findById(any()); + + assertTrue(file.isPresent()); + assertEquals(file.get(), this.testData.MOCK_MESSAGE_FILE); + + } + + @Test + void findFileById_notFound() { + + when(this.fileRepository.findById(this.FILE_ID_NOT_FOUND)).thenReturn(Optional.empty()); + + var file = this.service.findFileById(this.FILE_ID_NOT_FOUND); + + verify(this.fileRepository).findById(any()); + + assertTrue(file.isEmpty()); + + } + + @Test + void getHdContacts() { + + when(this.irisMessageClient.getIrisMessageHdContacts()).thenReturn(List.of(this.testData.MOCK_CONTACT_OTHER)); + + var contacts = this.service.getHdContacts(null); + + verify(this.irisMessageClient).getIrisMessageHdContacts(); + + assertEquals(contacts.size(), 1); + assertEquals(contacts.get(0), this.testData.MOCK_CONTACT_OTHER); + + } + + @Test + void getOwnHdContact() { + + when(this.irisMessageClient.getOwnIrisMessageHdContact()).thenReturn(this.testData.MOCK_CONTACT_OWN); + + var contact = this.service.getOwnHdContact(); + + verify(this.irisMessageClient).getOwnIrisMessageHdContact(); + + assertEquals(contact, this.testData.MOCK_CONTACT_OWN); + + } + + @Test + void sendMessage() { + + IrisMessage message = this.testData.MOCK_OUTBOX_MESSAGE; + + doNothing().when(this.irisMessageClient).createIrisMessage(any(IrisMessage.class)); + when(this.messageRepository.save(any(IrisMessage.class))).then(AdditionalAnswers.returnsFirstArg()); + + var sentMessage = this.service.sendMessage(message); + + verify(this.irisMessageClient).createIrisMessage(message); + verify(this.messageRepository).save(message); + + assertEquals(sentMessage, message); + + } + +} diff --git a/iris-client-bff/src/test/java/iris/client_bff/iris_messages/IrisMessageTestData.java b/iris-client-bff/src/test/java/iris/client_bff/iris_messages/IrisMessageTestData.java new file mode 100644 index 000000000..3dff5e963 --- /dev/null +++ b/iris-client-bff/src/test/java/iris/client_bff/iris_messages/IrisMessageTestData.java @@ -0,0 +1,100 @@ +package iris.client_bff.iris_messages; + +import iris.client_bff.iris_messages.web.IrisMessageInsertDto; +import org.springframework.stereotype.Component; + +@Component +public class IrisMessageTestData { + + public final IrisMessageFile MOCK_MESSAGE_FILE = getTestMessageFile(); + + private final IrisMessageFolder MOCK_DEFAULT_FOLDER = getTestDefaultMessageFolder(); + + public final IrisMessageFolder MOCK_INBOX_FOLDER = MOCK_DEFAULT_FOLDER; + public final IrisMessageFolder MOCK_OUTBOX_FOLDER = getTestMessageFolder(IrisMessageContext.OUTBOX, "outbox folder"); + + public final IrisMessageHdContact MOCK_CONTACT_OWN = getTestMessageHdContactOwn(); + public final IrisMessageHdContact MOCK_CONTACT_OTHER = getTestMessageHdContactOther(); + + public final IrisMessage MOCK_INBOX_MESSAGE = getTestInboxMessage(MOCK_INBOX_FOLDER); + public final IrisMessage MOCK_OUTBOX_MESSAGE = getTestOutboxMessage(MOCK_OUTBOX_FOLDER); + + public final static String INVALID_SUBJECT = "S".repeat(Math.max(0, IrisMessage.SUBJECT_MAX_LENGTH + 1)); + public final static String INVALID_BODY = "B".repeat(Math.max(0, IrisMessage.BODY_MAX_LENGTH + 1)); + + private IrisMessageFile getTestMessageFile() { + return new IrisMessageFile() + .setName("test-file-name") + .setContent("test".getBytes()); + } + + private IrisMessageHdContact getTestMessageHdContactOwn() { + return new IrisMessageHdContact() + .setName("test-own-contact") + .setId("test-own-contact-id") + .setIsOwn(true); + } + + private IrisMessageHdContact getTestMessageHdContactOther() { + return new IrisMessageHdContact() + .setName("test-other-contact") + .setId("test-other-contact-id") + .setIsOwn(false); + } + + private IrisMessageFolder getTestDefaultMessageFolder() { + IrisMessageFolder folder = new IrisMessageFolder() + .setContext(IrisMessageContext.INBOX) + .setName("default folder"); + folder.setDefaultFolder(folder.getId()); + return folder; + } + + public IrisMessageFolder getTestMessageFolder(IrisMessageFolder parentFolder, String name) { + return getTestMessageFolder(parentFolder.getContext(), name) + .setParentFolder(parentFolder.getId()); + } + + private IrisMessageFolder getTestMessageFolder(IrisMessageContext context, String name) { + return new IrisMessageFolder() + .setContext(context) + .setName(name) + .setDefaultFolder(MOCK_DEFAULT_FOLDER.getId()); + } + + public IrisMessage getTestInboxMessage() { + IrisMessageFolder folder = this.getTestMessageFolder(IrisMessageContext.INBOX, "inbox folder"); + return this.getTestInboxMessage(folder); + } + + public IrisMessage getTestOutboxMessage(IrisMessageFolder folder) { + IrisMessage message = new IrisMessage(); + message + .setSubject("Test outbox subject") + .setBody("Test outbox body") + .setFolder(folder) + .setHdAuthor(this.getTestMessageHdContactOwn()) + .setHdRecipient(this.getTestMessageHdContactOther()) + .setIsRead(true); + return message; + } + + public IrisMessage getTestInboxMessage(IrisMessageFolder folder) { + IrisMessage message = new IrisMessage(); + message + .setSubject("Test inbox subject") + .setBody("Test inbox body") + .setFolder(folder) + .setHdAuthor(this.getTestMessageHdContactOther()) + .setHdRecipient(this.getTestMessageHdContactOwn()) + .setIsRead(false); + return message; + } + + public IrisMessageInsertDto getTestMessageInsert(IrisMessage message) { + return new IrisMessageInsertDto() + .setSubject(message.getSubject()) + .setBody(message.getBody()) + .setHdRecipient(message.getHdRecipient().getId()); + } +} diff --git a/iris-client-bff/src/test/java/iris/client_bff/iris_messages/eps/IrisMessageDataControllerTest.java b/iris-client-bff/src/test/java/iris/client_bff/iris_messages/eps/IrisMessageDataControllerTest.java new file mode 100644 index 000000000..8b65d23ab --- /dev/null +++ b/iris-client-bff/src/test/java/iris/client_bff/iris_messages/eps/IrisMessageDataControllerTest.java @@ -0,0 +1,99 @@ +package iris.client_bff.iris_messages.eps; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import iris.client_bff.IrisWebIntegrationTest; +import iris.client_bff.iris_messages.IrisMessage; +import iris.client_bff.iris_messages.IrisMessageContext; +import iris.client_bff.iris_messages.IrisMessageException; +import iris.client_bff.iris_messages.IrisMessageFolder; +import iris.client_bff.iris_messages.IrisMessageFolderRepository; +import iris.client_bff.iris_messages.IrisMessageRepository; +import iris.client_bff.iris_messages.IrisMessageTestData; +import iris.client_bff.ui.messages.ErrorMessages; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.support.MessageSourceAccessor; + +@IrisWebIntegrationTest +class IrisMessageDataControllerTest { + + @Autowired + IrisMessageDataController dataController; + + @Autowired + IrisMessageRepository messageRepository; + + @Autowired + IrisMessageFolderRepository folderRepository; + + @Autowired + EPSIrisMessageClient messageClient; + + @Autowired + MessageSourceAccessor messages; + + @Test + void createIrisMessage() { + + IrisMessage localMessage = this.messageRepository.save(this.getMessage()); + IrisMessageTransferDto localMessageTransfer = IrisMessageTransferDto.fromEntity(localMessage); + + IrisMessageTransferDto remoteMessageTransfer = this.dataController.createIrisMessage(localMessageTransfer); + + assertNotNull(remoteMessageTransfer); + assertThat(localMessageTransfer).isEqualTo(remoteMessageTransfer); + } + + @Test + void createIrisMessage_shouldFail_noData() { + + var e = assertThrows(IrisMessageException.class, () -> this.dataController.createIrisMessage(null)); + + assertNotNull(e.getMessage()); + assertThat(e.getMessage()).contains(messages.getMessage("iris_message.invalid_id")); + } + + @Test + void createIrisMessage_shouldFail_invalidData() { + + IrisMessageTransferDto localMessageTransfer = Mockito.spy(IrisMessageTransferDto.fromEntity(this.getMessage())); + + localMessageTransfer.setSubject(IrisMessageTestData.INVALID_SUBJECT); + verify(localMessageTransfer).setSubject(IrisMessageTestData.INVALID_SUBJECT); + + localMessageTransfer.setBody(IrisMessageTestData.INVALID_BODY); + verify(localMessageTransfer).setBody(IrisMessageTestData.INVALID_BODY); + + var e = assertThrows(IrisMessageException.class, + () -> this.dataController.createIrisMessage(localMessageTransfer)); + + assertNotNull(e.getMessage()); + assertTrue(e.getMessage().contains(ErrorMessages.INVALID_INPUT)); + } + + private IrisMessage getMessage() { + + IrisMessageTestData testData = new IrisMessageTestData(); + + Optional outboxFolder = this.folderRepository + .findFirstByContextAndParentFolderIsNull(IrisMessageContext.OUTBOX); + + assertThat(outboxFolder.isPresent()).isTrue(); + + IrisMessage message = testData.getTestOutboxMessage(outboxFolder.get()); + + // @todo: remove next line as soon as dummy loopback functionality is removed / EPS message endpoints are + // implemented + message.setHdRecipient(this.messageClient.getOwnIrisMessageHdContact()); + + return message; + } + +} diff --git a/iris-client-bff/src/test/java/iris/client_bff/iris_messages/web/IrisMessageControllerIntegrationTest.java b/iris-client-bff/src/test/java/iris/client_bff/iris_messages/web/IrisMessageControllerIntegrationTest.java new file mode 100644 index 000000000..415d22a5c --- /dev/null +++ b/iris-client-bff/src/test/java/iris/client_bff/iris_messages/web/IrisMessageControllerIntegrationTest.java @@ -0,0 +1,45 @@ +package iris.client_bff.iris_messages.web; + +import static io.restassured.module.mockmvc.RestAssuredMockMvc.*; +import static org.hamcrest.Matchers.*; + +import io.restassured.http.ContentType; +import iris.client_bff.IrisWebIntegrationTest; +import iris.client_bff.iris_messages.IrisMessageDataInitializer; +import lombok.RequiredArgsConstructor; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +/** + * @author Jens Kutzsche + */ +@IrisWebIntegrationTest +@RequiredArgsConstructor +class IrisMessageControllerIntegrationTest { + + private final String baseUrl = "/iris-messages"; + + private final MockMvc mockMvc; + private final IrisMessageDataInitializer initializer; + + @Test + @WithMockUser() + @DisplayName("Tests getMessage to search with folder and search string") + void getMessage_WithFolderAndSearchString_ReturnsMessage() throws Exception { + + given().mockMvc(mockMvc) + .param("folder", initializer.getInboxFolder().getId().toString()) + .param("search", "First test") + .when() + .get(baseUrl) + .then() + .status(HttpStatus.OK) + .contentType(ContentType.JSON) + .body("content", hasSize(1)) + .body("content[0].subject", is("First test inbox subject")); + } +} diff --git a/iris-client-bff/src/test/java/iris/client_bff/iris_messages/web/IrisMessageControllerTest.java b/iris-client-bff/src/test/java/iris/client_bff/iris_messages/web/IrisMessageControllerTest.java new file mode 100644 index 000000000..086e4cce1 --- /dev/null +++ b/iris-client-bff/src/test/java/iris/client_bff/iris_messages/web/IrisMessageControllerTest.java @@ -0,0 +1,365 @@ +package iris.client_bff.iris_messages.web; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; + +import iris.client_bff.IrisWebIntegrationTest; +import iris.client_bff.RestResponsePage; +import iris.client_bff.events.web.TestData; +import iris.client_bff.iris_messages.IrisMessage; +import iris.client_bff.iris_messages.IrisMessage.IrisMessageIdentifier; +import iris.client_bff.iris_messages.IrisMessageBuilder; +import iris.client_bff.iris_messages.IrisMessageFile.IrisMessageFileIdentifier; +import iris.client_bff.iris_messages.IrisMessageFolder; +import iris.client_bff.iris_messages.IrisMessageFolder.IrisMessageFolderIdentifier; +import iris.client_bff.iris_messages.IrisMessageHdContact; +import iris.client_bff.iris_messages.IrisMessageService; +import iris.client_bff.iris_messages.IrisMessageTestData; +import lombok.RequiredArgsConstructor; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +@IrisWebIntegrationTest +@RequiredArgsConstructor +class IrisMessageControllerTest { + + private final String baseUrl = "/iris-messages"; + + TypeReference> PAGE_TYPE = new TypeReference<>() {}; + + private final MockMvc mockMvc; + private final ObjectMapper om; + + private final IrisMessageTestData testData; + + @MockBean + private IrisMessageService irisMessageService; + + @MockBean + private IrisMessageBuilder irisMessageBuilder; + + @Test + void endpointShouldBeProtected() throws Exception { + mockMvc.perform(get(baseUrl)) + .andExpect(MockMvcResultMatchers.status().isForbidden()) + .andReturn(); + } + + @Test + @WithMockUser() + void getInboxMessages() throws Exception { + this.getMessages(testData.MOCK_INBOX_MESSAGE, testData.MOCK_INBOX_MESSAGE.getFolder().getId()); + } + + @Test + @WithMockUser() + void getOutboxMessages() throws Exception { + this.getMessages(testData.MOCK_OUTBOX_MESSAGE, testData.MOCK_OUTBOX_MESSAGE.getFolder().getId()); + } + + @Test + @WithMockUser() + void getMessages_shouldFail() throws Exception { + mockMvc.perform(get(baseUrl)) + .andExpect(MockMvcResultMatchers.status().is4xxClientError()) + .andReturn(); + } + + @Test + @WithMockUser() + public void createAndSendMessage() throws Exception { + IrisMessage irisMessage = testData.MOCK_OUTBOX_MESSAGE; + + IrisMessageInsertDto messageInsert = this.testData.getTestMessageInsert(irisMessage); + + when(irisMessageBuilder.build(any(IrisMessageInsertDto.class))).thenReturn(irisMessage); + + when(irisMessageService.sendMessage(any())).thenReturn(irisMessage); + when(irisMessageService.findById(irisMessage.getId())).thenReturn(Optional.of(irisMessage)); + + ObjectMapper objectMapper = new ObjectMapper(); + + var postResult = mockMvc + .perform( + MockMvcRequestBuilders.post(baseUrl) + .content(objectMapper.writeValueAsString(messageInsert)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andReturn(); + + verify(irisMessageBuilder).build(messageInsert); + verify(irisMessageService).sendMessage(any()); + + String location = postResult.getResponse().getHeader("location"); + assert location != null; + String messageId = location.substring(location.lastIndexOf('/') + 1); + + var res = mockMvc.perform(get(baseUrl + "/" + messageId)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + verify(irisMessageService).findById(any()); + + var messageDetailsDto = om.readValue(res.getResponse().getContentAsString(), IrisMessageDetailsDto.class); + + assertThat(messageDetailsDto.getHdRecipient().getId()).isEqualTo(messageInsert.getHdRecipient()); + assertThat(messageDetailsDto.getSubject()).isEqualTo(messageInsert.getSubject()); + assertThat(messageDetailsDto.getBody()).isEqualTo(messageInsert.getBody()); + + } + + @Test + @WithMockUser() + public void createMessage_shouldFail() throws Exception { + IrisMessage irisMessage = testData.MOCK_OUTBOX_MESSAGE; + mockMvc + .perform( + MockMvcRequestBuilders + .multipart(baseUrl) + .param("hdRecipient", irisMessage.getHdRecipient().getId()) + .param("subject", IrisMessageTestData.INVALID_SUBJECT) + .param("body", IrisMessageTestData.INVALID_BODY)) + .andExpect(MockMvcResultMatchers.status().is4xxClientError()) + .andReturn(); + + } + + @Test + @WithMockUser() + void getMessageDetails() throws Exception { + + IrisMessageIdentifier messageId = testData.MOCK_INBOX_MESSAGE.getId(); + + when(irisMessageService.findById(messageId)).thenReturn(Optional.of(testData.MOCK_INBOX_MESSAGE)); + + var res = mockMvc.perform(get(baseUrl + "/" + messageId)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + verify(irisMessageService).findById(messageId); + + var messageDetailsDto = om.readValue(res.getResponse().getContentAsString(), IrisMessageDetailsDto.class); + + assertThat(messageDetailsDto) + .isEqualTo(IrisMessageDetailsDto.fromEntity(testData.MOCK_INBOX_MESSAGE)); + } + + @Test + @WithMockUser() + void getMessageDetails_shouldFail() throws Exception { + + IrisMessageIdentifier invalidId = IrisMessageIdentifier.of(UUID.randomUUID()); + + when(irisMessageService.findById(invalidId)).thenReturn(Optional.empty()); + + var res = mockMvc.perform(get(baseUrl + "/" + invalidId)) + .andExpect(MockMvcResultMatchers.status().is4xxClientError()) + .andReturn(); + + verify(irisMessageService).findById(invalidId); + + assertThat(res.getResponse().getContentAsString()).isEmpty(); + } + + @Test + @WithMockUser() + void updateMessage() throws Exception { + IrisMessageUpdateDto messageUpdate = new IrisMessageUpdateDto(true); + + IrisMessage updatedMessage = spy(testData.getTestInboxMessage()); + updatedMessage.setIsRead(true); + verify(updatedMessage).setIsRead(true); + + when(irisMessageService.findById(any())).thenReturn(Optional.of(updatedMessage)); + when(irisMessageService.saveMessage(updatedMessage)).thenReturn(updatedMessage); + + var res = mockMvc + .perform( + MockMvcRequestBuilders.patch(baseUrl + "/" + updatedMessage.getId()) + .content(om.writeValueAsString(messageUpdate)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + verify(irisMessageService).findById(any()); + verify(irisMessageService).saveMessage(any(IrisMessage.class)); + + var messageDetailsDto = om.readValue(res.getResponse().getContentAsString(), IrisMessageDetailsDto.class); + + assertEquals(messageDetailsDto.getIsRead(), true); + assertThat(messageDetailsDto).isEqualTo(IrisMessageDetailsDto.fromEntity(updatedMessage)); + } + + @Test + @WithMockUser() + void updateMessage_shouldFail() throws Exception { + UUID invalidId = UUID.randomUUID(); + + IrisMessageUpdateDto messageUpdate = new IrisMessageUpdateDto(true); + + when(irisMessageService.findById(any())).thenReturn(Optional.empty()); + + mockMvc + .perform( + MockMvcRequestBuilders.patch(baseUrl + "/" + invalidId) + .content(om.writeValueAsString(messageUpdate)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().is4xxClientError()) + .andReturn(); + } + + @Test + @WithMockUser() + void getMessageFolders() throws Exception { + + List folderList = List.of(testData.MOCK_INBOX_FOLDER, testData.MOCK_OUTBOX_FOLDER); + + when(irisMessageService.getFolders()).thenReturn(folderList); + + var res = mockMvc.perform(get(baseUrl + "/folders")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + verify(irisMessageService).getFolders(); + + List folderDtoList = om.readValue(res.getResponse().getContentAsString(), + new TypeReference<>() {}); + + assertEquals(2, folderDtoList.size()); + assertThat(folderDtoList).isEqualTo(IrisMessageFolderDto.fromEntity(folderList)); + } + + @Test + @WithMockUser() + void downloadMessageFile() throws Exception { + when(irisMessageService.findFileById(any(IrisMessageFileIdentifier.class))).thenReturn(Optional.of(testData.MOCK_MESSAGE_FILE)); + + var res = mockMvc + .perform(MockMvcRequestBuilders.get(baseUrl + "/files/{id}/download", testData.MOCK_MESSAGE_FILE.getId())) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + verify(irisMessageService).findFileById(any(IrisMessageFileIdentifier.class)); + + assertThat(res.getResponse().getHeader(HttpHeaders.CONTENT_DISPOSITION)).contains(testData.MOCK_MESSAGE_FILE.getName()); + assertThat(res.getResponse().getHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS)).isEqualTo(HttpHeaders.CONTENT_DISPOSITION); + } + + @Test + @WithMockUser() + void getMessageHdContactsWithoutOwn() throws Exception { + + when(irisMessageService.getHdContacts(null)).thenReturn(List.of(testData.MOCK_CONTACT_OTHER)); + + var res = mockMvc + .perform(get(baseUrl + "/hd-contacts")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + verify(irisMessageService).getHdContacts(null); + + List contacts = om.readValue(res.getResponse().getContentAsString(), + new TypeReference<>() {}); + + assertEquals(1, contacts.size()); + assertThat(contacts).contains(testData.MOCK_CONTACT_OTHER); + assertThat(contacts).doesNotContain(testData.MOCK_CONTACT_OWN); + } + + @Test + @WithMockUser() + void getMessageHdContactsIncludingOwn() throws Exception { + + when(irisMessageService.getHdContacts(null)).thenReturn(List.of(testData.MOCK_CONTACT_OTHER)); + when(irisMessageService.getOwnHdContact()).thenReturn(testData.MOCK_CONTACT_OWN); + + var res = mockMvc + .perform(get(baseUrl + "/hd-contacts").queryParam("includeOwn", "true")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + verify(irisMessageService).getHdContacts(null); + verify(irisMessageService).getOwnHdContact(); + + List contacts = om.readValue(res.getResponse().getContentAsString(), + new TypeReference<>() {}); + + assertEquals(2, contacts.size()); + assertThat(contacts).contains(testData.MOCK_CONTACT_OTHER); + assertThat(contacts).contains(testData.MOCK_CONTACT_OWN); + } + + @Test + @WithMockUser() + void getUnreadMessageCount() throws Exception { + when(irisMessageService.getCountUnread()).thenReturn(2); + + var res = mockMvc + .perform(get(baseUrl + "/count/unread")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + verify(irisMessageService).getCountUnread(); + + Integer count = om.readValue(res.getResponse().getContentAsString(), new TypeReference<>() {}); + + assertEquals(2, count); + } + + @Test + @WithMockUser() + void getUnreadMessageCountByFolder() throws Exception { + when(irisMessageService.getCountUnreadByFolderId(any())).thenReturn(1); + + var res = mockMvc + .perform(get(baseUrl + "/count/unread") + .queryParam("folder", testData.MOCK_INBOX_FOLDER.getId().toString())) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + verify(irisMessageService).getCountUnreadByFolderId(testData.MOCK_INBOX_FOLDER.getId()); + + Integer count = om.readValue(res.getResponse().getContentAsString(), new TypeReference<>() {}); + + assertEquals(1, count); + } + + private void getMessages(IrisMessage message, IrisMessageFolderIdentifier folderId) throws Exception { + + when(irisMessageService.search(eq(folderId), nullable(String.class), any(Pageable.class))) + .thenReturn(new RestResponsePage<>(List.of(message))); + + var res = mockMvc.perform(get(baseUrl) + .param("folder", folderId.toString())) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + + verify(irisMessageService).search(eq(folderId), nullable(String.class), any(Pageable.class)); + + var messagesPage = om.readValue(res.getResponse().getContentAsString(), PAGE_TYPE); + + assertEquals(1, messagesPage.getContent().size()); + assertThat(messagesPage.getContent().get(0)).isEqualTo(IrisMessageListItemDto.fromEntity(message)); + + } + +} diff --git a/iris-client-bff/src/test/resources/messages_de.properties b/iris-client-bff/src/test/resources/messages_de_DE_test.properties similarity index 100% rename from iris-client-bff/src/test/resources/messages_de.properties rename to iris-client-bff/src/test/resources/messages_de_DE_test.properties diff --git a/iris-client-fe/src/App.vue b/iris-client-fe/src/App.vue index 9233e9e53..b5a8fa622 100644 --- a/iris-client-fe/src/App.vue +++ b/iris-client-fe/src/App.vue @@ -3,26 +3,33 @@