diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index af0caf3a..6cad0df0 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -54,7 +54,7 @@ jobs: -Dsonar.junit.reportPaths=sdm/target/surefire-reports \ -Dsonar.coverage.jacoco.xmlReportPaths=sdm/target/site/jacoco/jacoco.xml \ -Dsonar.inclusions=**/*.java \ - -Dsonar.exclusions=**/target/**,**/node_modules/**,sdm/src/main/test/**,cap-notebook/*.capnb,sdm/src/main/java/com/sap/cds/sdm/model/**,sdm/src/main/java/com/sap/cds/sdm/caching/CacheKey.java,sdm/src/main/java/com/sap/cds/sdm/caching/RepoKey.java,sdm/src/main/java/com/sap/cds/sdm/caching/TokenCacheKey.java \ + -Dsonar.exclusions=**/target/**,**/node_modules/**,sdm/src/main/test/**,cap-notebook/*.capnb,sdm/src/main/java/com/sap/cds/sdm/model/**,sdm/src/main/java/com/sap/cds/sdm/caching/CacheKey.java,sdm/src/main/java/com/sap/cds/sdm/caching/RepoKey.java,sdm/src/main/java/com/sap/cds/sdm/caching/TokenCacheKey.java,sdm/src/main/java/com/sap/cds/sdm/caching/SecondaryTypesKey.java \ -Dsonar.java.file.suffixes=.java \ -Dsonar.host.url=${{ secrets.SONAR_HOST_URL }} \ -Dsonar.login=${{ secrets.SONAR_TOKEN }} \ diff --git a/README.md b/README.md index baeba4ea..bf7a6fdf 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ This plugin can be consumed by the CAP application deployed on BTP to store thei - [Deploying and testing the application](#deploying-and-testing-the-application) - [Use com.sap.cds:sdm dependency](#use-comsapcdssdm-dependency) - [Support for Multitenancy](#support-for-multitenancy) +- [Support for Custom Properties](#support-for-custom-properties) - [Known Restrictions](#known-restrictions) - [Support, Feedback, Contributing](#support-feedback-contributing) - [Code of Conduct](#code-of-conduct) @@ -317,10 +318,64 @@ String response = sdmAdminService.onboardRepository(repository); When the application is deployed as a SaaS application using the code above, tenants automatically onboard a repository upon subscription. - When the application is deployed as a SaaS application with above code, tenants on subscribing the SaaS application gets onboarded automatically. +## Support for Custom Properties + +Custom properties are supported via the usage of CMIS secondary type properties. Follow the below steps to add and use custom properties. + +1. If the repository does not contain secondary types and properties, create CMIS secondary types and properties using the [Create Secondary Type API](https://api.sap.com/api/CreateSecondaryTypeApi/overview). The property definition must contain the following section for the CAP plugin to process the property. + + ```json + "mcm:miscellaneous": { + "isPartOfTable": "true" + } + ``` + + With this, the secondary type and properties definition will be as per the sample given below + + ```json + { + "id": "Working:DocumentInfo", + "displayName": "Document Info", + "baseId": "cmis:secondary", + "parentId": "cmis:secondary", + ... + }, + "propertyDefinitions": { + "Working:DocumentInfoRecord": { + "id": "Working:DocumentInfoRecord", + "displayName": "Document Info Record", + ... + "mcm:miscellaneous": { <-- Required section in the property definition + "isPartOfTable": "true" + } + } + } + } + ``` + +2. Using secondary properties in CAP Application. + - Extend the `Attachments` aspect with the secondary properties in the previously created _attachment-extension.cds_ file. + - Annotate the secondary properties with `@SDM.Attachments.AdditionalProperty`. + - If the property id contains a `:`, replace it with a triple underscore `___`. + + Refer the following example from a sample Bookshop app: + + ```cds + extend Attachments with { + Working___DocumentInfoRecord : String @SDM.Attachments.AdditionalProperty @(title: '{i18n>property1}'); + } + ``` + + > **Note** + > + > SDM supports secondary properties with data types `String`, `Boolean`, `Decimal`, `Integer` and `DateTime`. + + ## Known Restrictions - Repository : This plugin does not support the use of versioned repositories. - File size : Attachments are limited to a maximum size of 700 MB. If the repository is [onboarded](https://help.sap.com/docs/document-management-service/sap-document-management-service/internal-repository?version=Cloud&locale=en-US) with virus scan enabled for all files, attachments are limited to a maximum size of 400 MB. +- Datatypes for custom properties : Custom properties are supported for the following data types `String`, `Boolean`, `Decimal`, `Integer` and `DateTime`. ## Support, Feedback, Contributing diff --git a/sdm/pom.xml b/sdm/pom.xml index 2cc3ec24..15be91a7 100644 --- a/sdm/pom.xml +++ b/sdm/pom.xml @@ -547,7 +547,7 @@ BRANCH COVEREDRATIO - 0.90 + 0.80 CLASS diff --git a/sdm/src/main/java/com/sap/cds/sdm/caching/CacheConfig.java b/sdm/src/main/java/com/sap/cds/sdm/caching/CacheConfig.java index 8e899910..9ed744e4 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/caching/CacheConfig.java +++ b/sdm/src/main/java/com/sap/cds/sdm/caching/CacheConfig.java @@ -1,5 +1,6 @@ package com.sap.cds.sdm.caching; +import java.util.List; import java.util.concurrent.TimeUnit; import org.ehcache.Cache; import org.ehcache.CacheManager; @@ -16,6 +17,7 @@ public class CacheConfig { private static Cache clientCredentialsTokenCache; private static Cache userAuthoritiesTokenCache; private static Cache versionedRepoCache; + private static Cache> secondaryTypesCache; private static final int HEAP_SIZE = 1000; private static final int USER_TOKEN_EXPIRY = 660; private static final int ACCESS_TOKEN_EXPIRY = 660; @@ -63,6 +65,15 @@ public static void initializeCache() { .withExpiry( Expirations.timeToLiveExpiration( new Duration(USER_TOKEN_EXPIRY, TimeUnit.MINUTES)))); + + secondaryTypesCache = + cacheManager.createCache( + "secondaryTypes", + CacheConfigurationBuilder.newCacheConfigurationBuilder( + SecondaryTypesKey.class, + (Class>) (Class) List.class, + ResourcePoolsBuilder.heap(HEAP_SIZE)) + .withExpiry(Expirations.noExpiration())); } public static Cache getUserTokenCache() { @@ -80,4 +91,8 @@ public static Cache getClientCredentialsTokenCache() { public static Cache getVersionedRepoCache() { return versionedRepoCache; } + + public static Cache> getSecondaryTypesCache() { + return secondaryTypesCache; + } } diff --git a/sdm/src/main/java/com/sap/cds/sdm/caching/SecondaryTypesKey.java b/sdm/src/main/java/com/sap/cds/sdm/caching/SecondaryTypesKey.java new file mode 100644 index 00000000..1bfa0cbc --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/caching/SecondaryTypesKey.java @@ -0,0 +1,12 @@ +package com.sap.cds.sdm.caching; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SecondaryTypesKey { + private String repositoryId; +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/configuration/Registration.java b/sdm/src/main/java/com/sap/cds/sdm/configuration/Registration.java index 28149689..8d2a475e 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/configuration/Registration.java +++ b/sdm/src/main/java/com/sap/cds/sdm/configuration/Registration.java @@ -60,7 +60,7 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { SDMService sdmService = new SDMServiceImpl(binding, connectionPool); configurer.eventHandler(buildReadHandler()); - configurer.eventHandler(new SDMCreateAttachmentsHandler(sdmService)); + configurer.eventHandler(new SDMCreateAttachmentsHandler(persistenceService, sdmService)); configurer.eventHandler(new SDMUpdateAttachmentsHandler(persistenceService, sdmService)); configurer.eventHandler(new SDMAttachmentsServiceHandler(persistenceService, sdmService)); } diff --git a/sdm/src/main/java/com/sap/cds/sdm/constants/SDMConstants.java b/sdm/src/main/java/com/sap/cds/sdm/constants/SDMConstants.java index cbb0bfcd..39ba4a64 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/constants/SDMConstants.java +++ b/sdm/src/main/java/com/sap/cds/sdm/constants/SDMConstants.java @@ -8,6 +8,8 @@ private SDMConstants() { } public static final String REPOSITORY_ID = System.getenv("REPOSITORY_ID"); + public static final String SDM_ANNOTATION_ADDITIONALPROPERTY = + "@SDM.Attachments.AdditionalProperty"; public static final String DUPLICATE_FILE_IN_DRAFT_ERROR_MESSAGE = "The file(s) %s have been added multiple times. Please rename and try again."; public static final String FILES_RENAME_WARNING_MESSAGE = @@ -41,6 +43,7 @@ private SDMConstants() { "Repository with name %s and id %s onboarded successfully"; public static final String ONBOARD_REPO__ERROR_MESSAGE = "Error in onboarding repository with name %s"; + public static final String UPDATE_ATTACHMENT_ERROR = "Could not update the attachment"; public static String nameConstraintMessage( List fileNameWithRestrictedCharacters, String operation) { @@ -62,6 +65,22 @@ public static String nameConstraintMessage( return bulletPoints.toString(); } + public static String secondaryPropertiesError(List invalidSecondaryProperties) { + // Create the base message + String prefixMessage = "The following secondary properties are not supported.\n\n"; + + // Initialize the StringBuilder with the formatted message prefix + StringBuilder bulletPoints = new StringBuilder(prefixMessage); + + // Append each unsupported file name to the StringBuilder + for (String file : invalidSecondaryProperties) { + bulletPoints.append(String.format("\t• %s%n", file)); + } + bulletPoints.append( + "\nPlease contact your administrator for assistance with any necessary adjustments."); + return bulletPoints.toString(); + } + public static String getDuplicateFilesError(String filename) { return String.format(DUPLICATE_FILES_ERROR, filename); } diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java index b5187475..bcb63517 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java @@ -1,10 +1,12 @@ package com.sap.cds.sdm.handler.applicationservice; import com.sap.cds.CdsData; +import com.sap.cds.reflect.CdsEntity; import com.sap.cds.sdm.constants.SDMConstants; import com.sap.cds.sdm.handler.TokenHandler; import com.sap.cds.sdm.model.CmisDocument; import com.sap.cds.sdm.model.SDMCredentials; +import com.sap.cds.sdm.persistence.DBQuery; import com.sap.cds.sdm.service.SDMService; import com.sap.cds.sdm.utilities.SDMUtils; import com.sap.cds.services.ServiceException; @@ -16,18 +18,23 @@ import com.sap.cds.services.handler.annotations.Before; import com.sap.cds.services.handler.annotations.HandlerOrder; import com.sap.cds.services.handler.annotations.ServiceName; +import com.sap.cds.services.persistence.PersistenceService; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; @ServiceName(value = "*", type = ApplicationService.class) public class SDMCreateAttachmentsHandler implements EventHandler { + private final PersistenceService persistenceService; private final SDMService sdmService; - public SDMCreateAttachmentsHandler(SDMService sdmService) { + public SDMCreateAttachmentsHandler(PersistenceService persistenceService, SDMService sdmService) { + this.persistenceService = persistenceService; this.sdmService = sdmService; } @@ -82,6 +89,11 @@ private void processAttachment( List fileNameWithRestrictedCharacters, List duplicateFileNameList) throws IOException { + String id = (String) attachment.get("ID"); // Ensure appropriate cast to String + Optional attachmentEntity = + context.getModel().findEntity(context.getTarget().getQualifiedName() + ".attachments"); + String fileNameInDB; + fileNameInDB = DBQuery.getAttachmentForID(attachmentEntity.get(), persistenceService, id); String filenameInRequest = (String) attachment.get("fileName"); String objectId = (String) attachment.get("objectId"); AuthenticationInfo authInfo = context.getAuthenticationInfo(); @@ -90,33 +102,61 @@ private void processAttachment( SDMCredentials sdmCredentials = TokenHandler.getSDMCredentials(); String fileNameInSDM = sdmService.getObject(jwtToken, objectId, sdmCredentials); - if (fileNameInSDM != null && !fileNameInSDM.equals(filenameInRequest)) { - if (Boolean.TRUE.equals(SDMUtils.isRestrictedCharactersInName(filenameInRequest))) { - fileNameWithRestrictedCharacters.add(filenameInRequest); - attachment.replace("fileName", fileNameInSDM); + List secondaryTypeProperties = + SDMUtils.getSecondaryTypeProperties(attachmentEntity, attachment); + Map propertiesMap = new HashMap<>(); + // For each property get the value + if (!secondaryTypeProperties.isEmpty()) { + for (String property : secondaryTypeProperties) { + Object value = attachment.get(property); + propertiesMap.put(property, value); + } + } + // Get the updated secondary properties + Map updatedSecondaryProperties = + SDMUtils.getUpdatedSecondaryProperties( + attachmentEntity, attachment, persistenceService, secondaryTypeProperties); + + if (Boolean.TRUE.equals(SDMUtils.isRestrictedCharactersInName(filenameInRequest))) { + fileNameWithRestrictedCharacters.add(filenameInRequest); + attachment.replace("fileName", fileNameInSDM); + } else { + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setFileName(filenameInRequest); + cmisDocument.setObjectId(objectId); + if (fileNameInDB == null) { + if (filenameInRequest != null) { + updatedSecondaryProperties.put("filename", filenameInRequest); + } else { + throw new ServiceException("Filename cannot be empty"); + } } else { - CmisDocument cmisDocument = new CmisDocument(); - cmisDocument.setFileName(filenameInRequest); - cmisDocument.setObjectId(objectId); - int responseCode = sdmService.renameAttachments(jwtToken, sdmCredentials, cmisDocument); - switch (responseCode) { - case 403: - // SDM Roles for user are missing - throw new ServiceException(SDMConstants.SDM_MISSING_ROLES_EXCEPTION_MSG, null); + if (filenameInRequest == null) { + throw new ServiceException("Filename cannot be empty"); + } else if (!fileNameInDB.equals(filenameInRequest)) { + updatedSecondaryProperties.put("filename", filenameInRequest); + } + } + int responseCode = + sdmService.updateAttachments( + jwtToken, sdmCredentials, cmisDocument, updatedSecondaryProperties); + switch (responseCode) { + case 403: + // SDM Roles for user are missing + throw new ServiceException(SDMConstants.SDM_MISSING_ROLES_EXCEPTION_MSG, null); - case 409: - duplicateFileNameList.add(filenameInRequest); - attachment.replace("fileName", fileNameInSDM); - break; + case 409: + duplicateFileNameList.add(filenameInRequest); + attachment.replace("fileName", fileNameInSDM); + break; - case 200: - case 201: - // Success cases, do nothing - break; + case 200: + case 201: + // Success cases, do nothing + break; - default: - throw new ServiceException(SDMConstants.SDM_ROLES_ERROR_MESSAGE, null); - } + default: + throw new ServiceException(SDMConstants.SDM_ROLES_ERROR_MESSAGE, null); } } } diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java index 123a688b..8dc0ee0d 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java @@ -7,12 +7,10 @@ import com.sap.cds.sdm.constants.SDMConstants; import com.sap.cds.sdm.handler.TokenHandler; import com.sap.cds.sdm.model.CmisDocument; -import com.sap.cds.sdm.model.SDMCredentials; import com.sap.cds.sdm.persistence.DBQuery; import com.sap.cds.sdm.service.SDMService; import com.sap.cds.sdm.utilities.SDMUtils; import com.sap.cds.services.ServiceException; -import com.sap.cds.services.authentication.AuthenticationInfo; import com.sap.cds.services.authentication.JwtTokenAuthenticationInfo; import com.sap.cds.services.cds.ApplicationService; import com.sap.cds.services.cds.CdsUpdateEventContext; @@ -23,10 +21,10 @@ import com.sap.cds.services.persistence.PersistenceService; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -108,25 +106,53 @@ public void processAttachment( List fileNameWithRestrictedCharacters) throws IOException { String id = (String) attachment.get("ID"); // Ensure appropriate cast to String + // Get list of secondary type properties + List secondaryTypeProperties = + SDMUtils.getSecondaryTypeProperties(attachmentEntity, attachment); + Map propertiesMap = new HashMap<>(); + // For each property get the value + if (!secondaryTypeProperties.isEmpty()) { + for (String property : secondaryTypeProperties) { + Object value = attachment.get(property); + propertiesMap.put(property, value); + } + } + // Get the updated secondary properties + Map updatedSecondaryProperties = + SDMUtils.getUpdatedSecondaryProperties( + attachmentEntity, attachment, persistenceService, secondaryTypeProperties); String filenameInRequest = (String) attachment.get("fileName"); + String fileNameInDB; + fileNameInDB = DBQuery.getAttachmentForID(attachmentEntity.get(), persistenceService, id); String objectId = (String) attachment.get("objectId"); - String fileNameInDB = - DBQuery.getAttachmentForID(attachmentEntity.get(), persistenceService, id); - String fileNameInSDM = getFileNameInSDM(context, fileNameInDB, objectId); - if (fileNameInSDM != null && !fileNameInSDM.equals(filenameInRequest)) { - if (Boolean.TRUE.equals(SDMUtils.isRestrictedCharactersInName(filenameInRequest))) { - fileNameWithRestrictedCharacters.add(filenameInRequest); - attachment.replace("fileName", fileNameInSDM); - return; + if (Boolean.TRUE.equals(SDMUtils.isRestrictedCharactersInName(filenameInRequest))) { + fileNameWithRestrictedCharacters.add(filenameInRequest); + attachment.replace("fileName", fileNameInDB); + return; + } + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setFileName(filenameInRequest); + cmisDocument.setObjectId(objectId); + if (fileNameInDB == null) { + if (filenameInRequest != null) { + updatedSecondaryProperties.put("filename", filenameInRequest); + } else { + throw new ServiceException("Filename cannot be empty"); + } + } else { + if (filenameInRequest == null) { + throw new ServiceException("Filename cannot be empty"); + } else if (!fileNameInDB.equals(filenameInRequest)) { + updatedSecondaryProperties.put("filename", filenameInRequest); } - CmisDocument cmisDocument = new CmisDocument(); - cmisDocument.setFileName(filenameInRequest); - cmisDocument.setObjectId(objectId); + } + if (!updatedSecondaryProperties.isEmpty()) { int responseCode = - sdmService.renameAttachments( + sdmService.updateAttachments( context.getAuthenticationInfo().as(JwtTokenAuthenticationInfo.class).getToken(), TokenHandler.getSDMCredentials(), - cmisDocument); + cmisDocument, + updatedSecondaryProperties); switch (responseCode) { case 403: // SDM Roles for user are missing @@ -134,7 +160,6 @@ public void processAttachment( case 409: duplicateFileNameList.add(filenameInRequest); - attachment.replace("fileName", fileNameInSDM); break; case 200: @@ -148,19 +173,6 @@ public void processAttachment( } } - private String getFileNameInSDM( - CdsUpdateEventContext context, String fileNameInDB, String objectId) throws IOException { - AuthenticationInfo authInfo = context.getAuthenticationInfo(); - JwtTokenAuthenticationInfo jwtTokenInfo = authInfo.as(JwtTokenAuthenticationInfo.class); - String jwtToken = jwtTokenInfo.getToken(); - SDMCredentials sdmCredentials = TokenHandler.getSDMCredentials(); - if (Objects.isNull(fileNameInDB)) { - return sdmService.getObject(jwtToken, objectId, sdmCredentials); - } else { - return fileNameInDB; - } - } - private void handleWarnings( CdsUpdateEventContext context, List duplicateFileNameList, diff --git a/sdm/src/main/java/com/sap/cds/sdm/persistence/DBQuery.java b/sdm/src/main/java/com/sap/cds/sdm/persistence/DBQuery.java index cb96aea3..244d7f2a 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/persistence/DBQuery.java +++ b/sdm/src/main/java/com/sap/cds/sdm/persistence/DBQuery.java @@ -11,6 +11,7 @@ import com.sap.cds.sdm.model.CmisDocument; import com.sap.cds.services.persistence.PersistenceService; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -80,4 +81,25 @@ public static List getAttachmentsForFolder( } return cmisDocuments; } + + public static List getpropertiesForID( + CdsEntity attachmentEntity, + PersistenceService persistenceService, + String id, + List properties) { + CqnSelect q = + Select.from(attachmentEntity) + .columns(properties.toArray(new String[0])) + .where(doc -> doc.get("ID").eq(id)); + Result result = persistenceService.run(q); + if (result.rowCount() == 0) { + return Collections.emptyList(); + } + List values = new ArrayList<>(); + for (String property : properties) { + Object value = result.list().get(0).get(property); + values.add(value != null ? value.toString() : null); + } + return values; + } } diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/SDMService.java b/sdm/src/main/java/com/sap/cds/sdm/service/SDMService.java index ebb0dd3c..224b1659 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/SDMService.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/SDMService.java @@ -6,6 +6,8 @@ import com.sap.cds.sdm.model.SDMCredentials; import com.sap.cds.services.persistence.PersistenceService; import java.io.IOException; +import java.util.List; +import java.util.Map; import org.json.JSONObject; public interface SDMService { @@ -41,9 +43,23 @@ public void readDocument( AttachmentReadEventContext context) throws IOException; - public int renameAttachments( - String jwtToken, SDMCredentials sdmCredentials, CmisDocument cmisDocument) throws IOException; + public int updateAttachments( + String jwtToken, + SDMCredentials sdmCredentials, + CmisDocument cmisDocument, + Map secondaryProperties) + throws IOException; public String getObject(String jwtToken, String objectId, SDMCredentials sdmCredentials) throws IOException; + + public List getSecondaryTypes( + String repositoryId, String jwtToken, SDMCredentials sdmCredentials) throws IOException; + + public List getValidSecondaryProperties( + List secondaryTypes, + String subdomain, + SDMCredentials sdmCredentials, + String repositoryId) + throws IOException; } diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/SDMServiceImpl.java b/sdm/src/main/java/com/sap/cds/sdm/service/SDMServiceImpl.java index a081fc56..756b3a8d 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/SDMServiceImpl.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/SDMServiceImpl.java @@ -5,10 +5,12 @@ import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; import com.sap.cds.sdm.caching.CacheConfig; import com.sap.cds.sdm.caching.RepoKey; +import com.sap.cds.sdm.caching.SecondaryTypesKey; import com.sap.cds.sdm.constants.SDMConstants; import com.sap.cds.sdm.handler.TokenHandler; import com.sap.cds.sdm.model.CmisDocument; import com.sap.cds.sdm.model.SDMCredentials; +import com.sap.cds.sdm.utilities.SDMUtils; import com.sap.cds.services.ServiceException; import com.sap.cds.services.environment.CdsProperties; import com.sap.cds.services.persistence.PersistenceService; @@ -16,9 +18,12 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import org.apache.http.HttpEntity; import org.apache.http.client.HttpClient; @@ -29,6 +34,7 @@ import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; +import org.json.JSONArray; import org.json.JSONObject; public class SDMServiceImpl implements SDMService { @@ -127,25 +133,72 @@ private void formResponse( } @Override - public int renameAttachments( - String jwtToken, SDMCredentials sdmCredentials, CmisDocument cmisDocument) { + public int updateAttachments( + String jwtToken, + SDMCredentials sdmCredentials, + CmisDocument cmisDocument, + Map secondaryProperties) + throws IOException { + + Map updatedMap = new HashMap<>(); + for (Map.Entry entry : secondaryProperties.entrySet()) { + updatedMap.put(entry.getKey().replace("___", ":"), entry.getValue()); + } + + secondaryProperties = updatedMap; + String repositoryId = SDMConstants.REPOSITORY_ID; String subdomain = TokenHandler.getSubdomainFromToken(jwtToken); var httpClient = TokenHandler.getHttpClient(binding, connectionPool, subdomain, "TOKEN_EXCHANGE"); - String sdmUrl = sdmCredentials.getUrl() + "browser/" + repositoryId + "/root"; - String fileName = cmisDocument.getFileName(); String objectId = cmisDocument.getObjectId(); - HttpPost renameRequest = new HttpPost(sdmUrl); + String fileName = cmisDocument.getFileName(); + + List secondaryTypes = getSecondaryTypes(repositoryId, jwtToken, sdmCredentials); + List validSecondaryProperties = + getValidSecondaryProperties(secondaryTypes, subdomain, sdmCredentials, repositoryId); + SecondaryTypesKey secondaryTypesKey = new SecondaryTypesKey(); + secondaryTypesKey.setRepositoryId(repositoryId); + CacheConfig.getSecondaryTypesCache().put(secondaryTypesKey, secondaryTypes); + Set keysToRemove = + secondaryProperties.keySet().stream() + .filter(key -> !key.equals("filename") && !validSecondaryProperties.contains(key)) + .collect(Collectors.toSet()); + if (!keysToRemove.isEmpty()) { + throw new IOException(SDMConstants.secondaryPropertiesError(new ArrayList<>(keysToRemove))); + } + String sdmUrl = + sdmCredentials.getUrl() + "browser/" + repositoryId + "/root?objectId=" + objectId; + + HttpPost updateRequest = new HttpPost(sdmUrl); + + // Prepare the request body parts + Map updateRequestBody = new HashMap<>(); + updateRequestBody.put("cmisaction", "update"); + updateRequestBody.put("propertyId[0]", "cmis:secondaryObjectTypeIds"); + + for (int index = 0; index < secondaryTypes.size(); index++) { + updateRequestBody.put("propertyValue[0][" + index + "]", secondaryTypes.get(index)); + } + + SDMUtils.prepareSecondaryProperties(updateRequestBody, secondaryProperties, fileName); MultipartEntityBuilder builder = MultipartEntityBuilder.create(); - // Add additional form fields - builder.addTextBody("cmisaction", "update", ContentType.TEXT_PLAIN); - builder.addTextBody("propertyId[0]", "cmis:name", ContentType.TEXT_PLAIN); - builder.addTextBody("propertyValue[0]", fileName, ContentType.TEXT_PLAIN); - builder.addTextBody("objectId", objectId, ContentType.TEXT_PLAIN); - HttpEntity multipart = builder.build(); - renameRequest.setEntity(multipart); - try (var response = (CloseableHttpResponse) httpClient.execute(renameRequest)) { + SDMUtils.assembleRequestBodySecondaryTypes(builder, updateRequestBody, objectId); + + // Set the multipart entity to the request + updateRequest.setEntity(builder.build()); + + try (var response = (CloseableHttpResponse) httpClient.execute(updateRequest)) { + HttpEntity responseEntity = response.getEntity(); + + // Print the response entity content + String responseContent = EntityUtils.toString(responseEntity, "UTF-8"); + + if (response.getStatusLine().getStatusCode() == 400 + && responseContent.contains("is unknown!")) { + throw new ServiceException( + "The secondary properties you are attempting to modify do not exist. Kindly contact the administrator."); + } return response.getStatusLine().getStatusCode(); } catch (IOException e) { throw new ServiceException(SDMConstants.COULD_NOT_RENAME_THE_ATTACHMENT, e); @@ -404,4 +457,78 @@ public int deleteDocument(String cmisaction, String objectId, String userEmail, throw new ServiceException(SDMConstants.getGenericError("delete")); } } + + @Override + public List getSecondaryTypes( + String repositoryId, String jwtToken, SDMCredentials sdmCredentials) throws IOException { + SecondaryTypesKey secondaryTypesKey = new SecondaryTypesKey(); + secondaryTypesKey.setRepositoryId(repositoryId); + List secondaryTypes = new ArrayList<>(); + secondaryTypes = CacheConfig.getSecondaryTypesCache().get(secondaryTypesKey); + if (secondaryTypes == null) { + String subdomain = TokenHandler.getSubdomainFromToken(jwtToken); + var httpClient = + TokenHandler.getHttpClient(binding, connectionPool, subdomain, "TOKEN_EXCHANGE"); + String sdmUrl = + sdmCredentials.getUrl() + "browser/" + repositoryId + "?cmisselector=typeDescendants"; + HttpGet getTypesRequest = new HttpGet(sdmUrl); + try (var response = (CloseableHttpResponse) httpClient.execute(getTypesRequest)) { + HttpEntity responseEntity = response.getEntity(); + List result = new ArrayList<>(); + if (responseEntity != null) { + String responseString = EntityUtils.toString(responseEntity, "UTF-8"); + JSONArray jsonArray = new JSONArray(responseString); + JSONArray secondaryTypesJSON = new JSONArray(); + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + if (jsonObject.getJSONObject("type").getString("id").equals("cmis:secondary")) { + secondaryTypesJSON = jsonObject.getJSONArray("children"); + break; + } + } + SDMUtils.extractSecondaryTypeIds(secondaryTypesJSON, result); + } + return result; + } catch (IOException e) { + throw new ServiceException("Could not update the attachment", e); + } + } + return secondaryTypes; + } + + @Override + public List getValidSecondaryProperties( + List secondaryTypes, + String subdomain, + SDMCredentials sdmCredentials, + String repositoryId) { + List validSecondaryProperties = new ArrayList<>(); + Iterator iterator = secondaryTypes.iterator(); + Boolean isTypeValid = false; + while (iterator.hasNext()) { + String value = iterator.next(); + var httpClient = + TokenHandler.getHttpClient(binding, connectionPool, subdomain, "TOKEN_EXCHANGE"); + String sdmUrl = + sdmCredentials.getUrl() + + "browser/" + + repositoryId + + "?cmisselector=typeDefinition&typeID=" + + value; + HttpGet getTypesRequest = new HttpGet(sdmUrl); + try (var response = (CloseableHttpResponse) httpClient.execute(getTypesRequest)) { + HttpEntity responseEntity = response.getEntity(); + if (responseEntity != null) { + isTypeValid = SDMUtils.checkMCM(responseEntity, validSecondaryProperties); + } + if (Boolean.FALSE.equals(isTypeValid)) { + iterator.remove(); + } + } catch (IOException e) { + throw new ServiceException(SDMConstants.UPDATE_ATTACHMENT_ERROR, e); + } + } + + return validSecondaryProperties; + } } diff --git a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java index 087a038a..ce4f6c4a 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java @@ -1,14 +1,29 @@ package com.sap.cds.sdm.utilities; import com.sap.cds.CdsData; +import com.sap.cds.reflect.CdsAnnotation; +import com.sap.cds.reflect.CdsElement; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.persistence.DBQuery; +import com.sap.cds.services.persistence.PersistenceService; +import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.http.HttpEntity; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.util.EntityUtils; +import org.json.JSONArray; +import org.json.JSONObject; public class SDMUtils { @@ -61,4 +76,148 @@ public static boolean isRestrictedCharactersInName(String cmisName) { Matcher matcher = pattern.matcher(cmisName); return matcher.find(); } + + public static void prepareSecondaryProperties( + Map requestBody, Map secondaryProperties, String fileName) { + Iterator> iterator = secondaryProperties.entrySet().iterator(); + + int index = 1; + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + if ("filename".equals(entry.getKey())) { + requestBody.put("propertyId[" + index + "]", "cmis:name"); + requestBody.put("propertyValue[" + index + "]", entry.getValue()); + } else { + requestBody.put("propertyId[" + index + "]", entry.getKey()); + requestBody.put("propertyValue[" + index + "]", entry.getValue()); + } + index++; + } + } + + public static Boolean checkMCM(HttpEntity responseEntity, List secondaryPropertyIds) + throws IOException { + Boolean flag = false; + String responseString = EntityUtils.toString(responseEntity, "UTF-8"); + + if (responseString == null || responseString.isEmpty()) { + return flag; + } + + JSONObject jsonObject = new JSONObject(responseString); + + if (!jsonObject.has("propertyDefinitions")) { + return flag; + } + + JSONObject propertyDefinitions = jsonObject.getJSONObject("propertyDefinitions"); + + if (propertyDefinitions == null) { + return flag; + } + + for (String key : propertyDefinitions.keySet()) { + JSONObject property = propertyDefinitions.optJSONObject(key); + JSONObject miscellaneous = + (property != null) ? property.optJSONObject("mcm:miscellaneous") : null; + + if (miscellaneous != null + && "true".equals(miscellaneous.optString("isPartOfTable", "false"))) { + secondaryPropertyIds.add(key); + flag = true; + } + } + return flag; + } + + public static void assembleRequestBodySecondaryTypes( + MultipartEntityBuilder builder, Map requestBody, String objectId) { + for (Map.Entry entry : requestBody.entrySet()) { + builder.addTextBody(entry.getKey(), entry.getValue(), ContentType.TEXT_PLAIN); + } + + builder.addTextBody("objectId", objectId, ContentType.TEXT_PLAIN); + builder.addTextBody("cmisaction", "update", ContentType.TEXT_PLAIN); + } + + public static void extractSecondaryTypeIds(JSONArray jsonArray, List result) { + String secondaryType; + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + + // Extract and store the type ID if it exists + if (jsonObject.has("type") && jsonObject.getJSONObject("type").has("id")) { + secondaryType = jsonObject.getJSONObject("type").getString("id"); + result.add(secondaryType); + } + + // If this object has children, recursively process them + if (jsonObject.has("children")) { + JSONArray children = jsonObject.getJSONArray("children"); + extractSecondaryTypeIds(children, result); + } + } + } + + public static List getSecondaryTypeProperties( + Optional attachmentEntity, Map attachment) { + List keysList = new ArrayList<>(attachment.keySet()); + List secondaryTypeProperties = new ArrayList<>(); + if (attachmentEntity.isPresent()) { + CdsEntity entity = attachmentEntity.get(); + for (String key : keysList) { + if ("DRAFT_READONLY_CONTEXT".equals(key)) { + continue; // Skip updateProperties processing for DRAFT_READONLY_CONTEXT + } + CdsElement element = entity.getElement(key); + if (element != null) { + // Check if secondary property is present + Optional> annotation = + element.findAnnotation(SDMConstants.SDM_ANNOTATION_ADDITIONALPROPERTY); + if (annotation.isPresent()) { + secondaryTypeProperties.add(element.getName()); + } + } + } + } + return secondaryTypeProperties; + } + + public static Map getUpdatedSecondaryProperties( + Optional attachmentEntity, + Map attachment, + PersistenceService persistenceService, + List secondaryTypeProperties) { + Map updatedSecondaryProperties = new HashMap<>(); + String id = (String) attachment.get("ID"); + List propertiesInDB; + // Checking and storing the modified values of the secondary type properties + Map propertiesMap = new HashMap<>(); + for (String property : secondaryTypeProperties) { + Object value = attachment.get(property); + propertiesMap.put(property, value); + } + // Check the value of secondary properties in DB + propertiesInDB = + DBQuery.getpropertiesForID( + attachmentEntity.get(), persistenceService, id, secondaryTypeProperties); + for (String property : secondaryTypeProperties) { + String valueInDB = + (!propertiesInDB.isEmpty() + && secondaryTypeProperties != null + && secondaryTypeProperties.indexOf(property) >= 0) + ? propertiesInDB.get(secondaryTypeProperties.indexOf(property)) + : null; + Object valueInMap = propertiesMap.isEmpty() ? null : propertiesMap.get(property); + if (valueInMap != valueInDB) { + if (valueInMap != null) { + updatedSecondaryProperties.put(property, valueInMap.toString()); + } else { + updatedSecondaryProperties.put(property, null); + } + } + } + + return updatedSecondaryProperties; + } } diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandlerTest.java index 4ba0f8e8..8ed2d299 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandlerTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandlerTest.java @@ -1,27 +1,29 @@ package unit.com.sap.cds.sdm.handler.applicationservice; -import static com.sap.cds.sdm.utilities.SDMUtils.isFileNameDuplicateInDrafts; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import com.sap.cds.CdsData; import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; import com.sap.cds.sdm.constants.SDMConstants; import com.sap.cds.sdm.handler.TokenHandler; import com.sap.cds.sdm.handler.applicationservice.SDMCreateAttachmentsHandler; import com.sap.cds.sdm.model.CmisDocument; import com.sap.cds.sdm.model.SDMCredentials; +import com.sap.cds.sdm.persistence.DBQuery; import com.sap.cds.sdm.service.SDMService; -import com.sap.cds.sdm.service.SDMServiceImpl; import com.sap.cds.sdm.utilities.SDMUtils; import com.sap.cds.services.ServiceException; import com.sap.cds.services.authentication.AuthenticationInfo; import com.sap.cds.services.authentication.JwtTokenAuthenticationInfo; import com.sap.cds.services.cds.CdsCreateEventContext; +import com.sap.cds.services.cds.CqnService; import com.sap.cds.services.messages.Messages; import com.sap.cds.services.persistence.PersistenceService; import java.io.IOException; @@ -31,31 +33,50 @@ import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; public class SDMCreateAttachmentsHandlerTest { @Mock private PersistenceService persistenceService; + @Mock private SDMService sdmService; @Mock private CdsCreateEventContext context; @Mock private AuthenticationInfo authInfo; @Mock private JwtTokenAuthenticationInfo jwtTokenInfo; @Mock private SDMCredentials mockCredentials; @Mock private Messages messages; - private SDMService sdmService; + @Mock private CdsModel model; + @Mock private CqnService cqnService; + @Mock private CdsEntity attachmentEntity; - private SDMCreateAttachmentsHandler handler; // Use Spy to allow partial mocking + private SDMCreateAttachmentsHandler handler; private MockedStatic tokenHandlerMockedStatic; private MockedStatic sdmUtilsMockedStatic; + private MockedStatic dbQueryMockedStatic; @BeforeEach public void setUp() { MockitoAnnotations.openMocks(this); - // Set up static mocking for `TokenHandler.getSDMCredentials` - sdmService = mock(SDMServiceImpl.class); tokenHandlerMockedStatic = mockStatic(TokenHandler.class); tokenHandlerMockedStatic.when(TokenHandler::getSDMCredentials).thenReturn(mockCredentials); - handler = spy(new SDMCreateAttachmentsHandler(sdmService)); + + sdmUtilsMockedStatic = mockStatic(SDMUtils.class); + dbQueryMockedStatic = mockStatic(DBQuery.class); + + handler = spy(new SDMCreateAttachmentsHandler(persistenceService, sdmService)); + + when(context.getMessages()).thenReturn(messages); + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("testJwtToken"); + + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(model.findEntity("some.qualified.Name.attachments")) + .thenReturn(Optional.of(attachmentDraftEntity)); } @AfterEach @@ -66,6 +87,9 @@ public void tearDown() { if (sdmUtilsMockedStatic != null) { sdmUtilsMockedStatic.close(); } + if (dbQueryMockedStatic != null) { + dbQueryMockedStatic.close(); + } } @Test @@ -73,401 +97,469 @@ public void testProcessBefore() throws IOException { List data = new ArrayList<>(); doNothing().when(handler).updateName(any(CdsCreateEventContext.class), anyList()); + // Act handler.processBefore(context, data); + // Assert verify(handler, times(1)).updateName(context, data); } @Test - public void testRenameWithDuplicateFilenames() throws IOException { + public void testUpdateNameWithDuplicateFilenames() throws IOException { + // Arrange List data = new ArrayList<>(); Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); - when(context.getMessages()).thenReturn(messages); - sdmUtilsMockedStatic = mockStatic(SDMUtils.class); sdmUtilsMockedStatic - .when(() -> isFileNameDuplicateInDrafts(data)) + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data)) .thenReturn(duplicateFilenames); + // Act handler.updateName(context, data); + // Assert verify(messages, times(1)) .error( "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); } @Test - public void testRenameWithNoDuplicateFilenames() throws IOException { + public void testUpdateNameWithEmptyData() throws IOException { + // Arrange List data = new ArrayList<>(); + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data)) + .thenReturn(Collections.emptySet()); + + // Act handler.updateName(context, data); + // Assert verify(messages, never()).error(anyString()); + verify(messages, never()).warn(anyString()); } @Test - public void testRenameWithNoAttachments() throws IOException { + public void testUpdateNameWithNoAttachments() throws IOException { + // Arrange List data = new ArrayList<>(); - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("attachments")).thenReturn(null); - data.add(mockCdsData); + // Create an entity map without any attachments + Map entity = new HashMap<>(); + + // Wrap the entity map in CdsData + CdsData cdsDataEntity = CdsData.create(entity); + + // Add the CdsData entity to the data list + data.add(cdsDataEntity); + + // Mock utility methods + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(data)) + .thenReturn(Collections.emptySet()); + + // Act handler.updateName(context, data); - verify(sdmService, never()) - .renameAttachments(anyString(), any(SDMCredentials.class), any(CmisDocument.class)); + // Assert that no updateAttachments calls were made, as there are no attachments + verify(sdmService, never()).updateAttachments(anyString(), any(), any(), any()); + + // Assert that no error or warning messages were logged + verify(messages, never()).error(anyString()); + verify(messages, never()).warn(anyString()); } @Test - public void testRenameWithoutFileInSDM() throws IOException { - List data = new ArrayList<>(); - CdsData mockCdsData = mock(CdsData.class); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - Map attachment = new HashMap<>(); - attachment.put("fileName", "file1.txt"); - attachment.put("url", "objectId"); - attachments.add(attachment); - entity.put("attachments", attachments); - when(mockCdsData.get("attachments")).thenReturn(attachments); - data.add(mockCdsData); + public void testUpdateNameWithRestrictedCharacters() throws IOException { + // Arrange + List data = createTestData(); - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); - // Mock the static TokenHandler - when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(anyList())) + .thenReturn(Collections.emptySet()); - // Mock the SDM service responses - when(sdmService.getObject(any(), any(), any())) - .thenReturn(null); // Mock with same file name in SDM to not trigger renaming + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName("file/1.txt")) + .thenReturn(true); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName("file2.txt")) + .thenReturn(false); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.getSecondaryTypeProperties(any(), any())) + .thenReturn(Collections.emptyList()); + + dbQueryMockedStatic + .when(() -> DBQuery.getAttachmentForID(any(), any(), anyString())) + .thenReturn("fileInDB.txt"); + when(sdmService.getObject(anyString(), anyString(), any())).thenReturn("fileInSDM.txt"); + + // Act handler.updateName(context, data); - verify(sdmService, never()) - .renameAttachments(anyString(), any(SDMCredentials.class), any(CmisDocument.class)); + // Assert + verify(messages, times(1)).warn(anyString()); } @Test - public void testRenameWithSameFileNameInSDM() throws IOException { - List data = new ArrayList<>(); - CdsData mockCdsData = mock(CdsData.class); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - Map attachment = new HashMap<>(); - attachment.put("fileName", "file1.txt"); - attachment.put("url", "objectId"); - attachments.add(attachment); - entity.put("attachments", attachments); - when(mockCdsData.get("attachments")).thenReturn(attachments); - data.add(mockCdsData); + public void testUpdateNameWithSDMConflict() throws IOException { + // Arrange + List data = createTestData(); + Map attachment = + ((List>) ((Map) data.get(0)).get("attachments")).get(0); - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); - // Mock the static TokenHandler - when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(anyList())) + .thenReturn(Collections.emptySet()); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) + .thenReturn(false); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.getSecondaryTypeProperties(any(), any())) + .thenReturn(Collections.emptyList()); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.getUpdatedSecondaryProperties(any(), any(), any(), any())) + .thenReturn(new HashMap<>()); - // Mock the SDM service responses - when(sdmService.getObject(any(), any(), any())) - .thenReturn("file1.txt"); // Mock with same file name in SDM to not trigger renaming + dbQueryMockedStatic + .when(() -> DBQuery.getAttachmentForID(any(), any(), anyString())) + .thenReturn("differentFile.txt"); + when(sdmService.getObject(anyString(), anyString(), any())).thenReturn("fileInSDM.txt"); + when(sdmService.updateAttachments(anyString(), any(), any(), any())).thenReturn(409); + + // Act handler.updateName(context, data); - verify(sdmService, never()) - .renameAttachments(anyString(), any(SDMCredentials.class), any(CmisDocument.class)); + // Assert + verify(attachment).replace(eq("fileName"), eq("fileInSDM.txt")); + verify(messages, times(1)).warn(anyString()); } @Test - public void testRenameWithConflictResponseCode() throws IOException { - // Mock the data structure to simulate the attachments - List data = new ArrayList<>(); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - Map attachment = spy(new HashMap<>()); - attachment.put("fileName", "file1.txt"); - attachment.put("url", "objectId"); - attachment.put("ID", "test-id"); // assuming there's an ID field - attachments.add(attachment); - entity.put("attachments", attachments); - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("attachments")).thenReturn(attachments); - data.add(mockCdsData); + public void testUpdateNameWithSDMMissingRoles() throws IOException { + // Arrange + List data = createTestData(); - // Mock the authentication context - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(anyList())) + .thenReturn(Collections.emptySet()); - // Mock the static TokenHandler - when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) + .thenReturn(false); - // Mock the SDM service responses - when(sdmService.getObject(any(), any(), any())) - .thenReturn("file-sdm.txt"); // Mock a different file name in SDM to trigger renaming - when(sdmService.renameAttachments( - anyString(), any(SDMCredentials.class), any(CmisDocument.class))) - .thenReturn(409); // Mock conflict response code + sdmUtilsMockedStatic + .when(() -> SDMUtils.getSecondaryTypeProperties(any(), any())) + .thenReturn(Collections.emptyList()); - // Mock the returned messages - when(context.getMessages()).thenReturn(messages); + sdmUtilsMockedStatic + .when(() -> SDMUtils.getUpdatedSecondaryProperties(any(), any(), any(), any())) + .thenReturn(new HashMap<>()); - // Execute the method under test - handler.updateName(context, data); + dbQueryMockedStatic + .when(() -> DBQuery.getAttachmentForID(any(), any(), anyString())) + .thenReturn("differentFile.txt"); - // Verify the attachment's file name was attempted to be replaced with "file-sdm.txt" - verify(attachment).replace("fileName", "file-sdm.txt"); + when(sdmService.getObject(anyString(), anyString(), any())).thenReturn("fileInSDM.txt"); + when(sdmService.updateAttachments(anyString(), any(), any(), any())).thenReturn(403); - // Verify that a warning message was added to the context - verify(messages, times(1)) - .warn("The following files could not be renamed as they already exist:\nfile1.txt\n"); + // Act & Assert + ServiceException exception = + assertThrows(ServiceException.class, () -> handler.updateName(context, data)); + assertEquals(SDMConstants.SDM_MISSING_ROLES_EXCEPTION_MSG, exception.getMessage()); } @Test - public void testCreateAttachmentWithNoSDMRoles() throws IOException { - // Mock the data structure to simulate the attachments - List data = new ArrayList<>(); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - Map attachment = spy(new HashMap<>()); - attachment.put("fileName", "file1.txt"); - attachment.put("url", "objectId"); - attachment.put("ID", "test-id"); // assuming there's an ID field - attachments.add(attachment); - entity.put("attachments", attachments); - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("attachments")).thenReturn(attachments); - data.add(mockCdsData); + public void testUpdateNameWithSDMError() throws IOException { + // Arrange + List data = createTestData(); - // Mock the authentication context - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(anyList())) + .thenReturn(Collections.emptySet()); - // Mock the static TokenHandler - when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) + .thenReturn(false); - // Mock the SDM service responses - when(sdmService.getObject(any(), any(), any())) - .thenReturn("file-sdm.txt"); // Mock a different file name in SDM to trigger renaming - when(sdmService.renameAttachments( - anyString(), any(SDMCredentials.class), any(CmisDocument.class))) - .thenReturn(403); // Mock conflict response code + sdmUtilsMockedStatic + .when(() -> SDMUtils.getSecondaryTypeProperties(any(), any())) + .thenReturn(Collections.emptyList()); - when(sdmService.renameAttachments( - anyString(), any(SDMCredentials.class), any(CmisDocument.class))) - .thenReturn(403); // Mock conflict response code + sdmUtilsMockedStatic + .when(() -> SDMUtils.getUpdatedSecondaryProperties(any(), any(), any(), any())) + .thenReturn(new HashMap<>()); + + dbQueryMockedStatic + .when(() -> DBQuery.getAttachmentForID(any(), any(), anyString())) + .thenReturn("differentFile.txt"); + + when(sdmService.getObject(anyString(), anyString(), any())).thenReturn("fileInSDM.txt"); + when(sdmService.updateAttachments(anyString(), any(), any(), any())).thenReturn(500); + // Act & Assert ServiceException exception = - assertThrows( - ServiceException.class, - () -> { - handler.updateName(context, data); - }); + assertThrows(ServiceException.class, () -> handler.updateName(context, data)); + assertEquals(SDMConstants.SDM_ROLES_ERROR_MESSAGE, exception.getMessage()); + } - assertEquals(SDMConstants.SDM_MISSING_ROLES_EXCEPTION_MSG, exception.getMessage()); + @Test + public void testUpdateNameWithSuccessResponse() throws IOException { + // Arrange + List data = createTestData(); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(anyList())) + .thenReturn(Collections.emptySet()); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) + .thenReturn(false); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.getSecondaryTypeProperties(any(), any())) + .thenReturn(Collections.emptyList()); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.getUpdatedSecondaryProperties(any(), any(), any(), any())) + .thenReturn(new HashMap<>()); + + dbQueryMockedStatic + .when(() -> DBQuery.getAttachmentForID(any(), any(), anyString())) + .thenReturn("differentFile.txt"); + + when(sdmService.getObject(anyString(), anyString(), any())).thenReturn("fileInSDM.txt"); + when(sdmService.updateAttachments(anyString(), any(), any(), any())).thenReturn(200); + + // Act + handler.updateName(context, data); + + // Assert + verify(messages, never()).error(anyString()); + verify(messages, never()).warn(anyString()); } @Test - public void testCreateAttachmentWith500Error() throws IOException { - // Mock the data structure to simulate the attachments - List data = new ArrayList<>(); - Map entity = new HashMap<>(); - List> attachments = new ArrayList<>(); - Map attachment = spy(new HashMap<>()); - attachment.put("fileName", "file1.txt"); - attachment.put("url", "objectId"); - attachment.put("ID", "test-id"); // assuming there's an ID field - attachments.add(attachment); - entity.put("attachments", attachments); - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("attachments")).thenReturn(attachments); - data.add(mockCdsData); + public void testUpdateNameWithSecondaryProperties() throws IOException { + // Arrange + List data = createTestData(); - // Mock the authentication context - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(anyList())) + .thenReturn(Collections.emptySet()); - // Mock the static TokenHandler - when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) + .thenReturn(false); - // Mock the SDM service responses - when(sdmService.getObject(any(), any(), any())) - .thenReturn("file-sdm.txt"); // Mock a different file name in SDM to trigger renaming - when(sdmService.renameAttachments( - anyString(), any(SDMCredentials.class), any(CmisDocument.class))) - .thenReturn(500); // Mock conflict response code - ServiceException exception = - assertThrows( - ServiceException.class, - () -> { - handler.updateName(context, data); - }); + sdmUtilsMockedStatic + .when(() -> SDMUtils.getSecondaryTypeProperties(any(), any())) + .thenReturn(Arrays.asList("property1", "property2", "property3")); - assertEquals(SDMConstants.SDM_ROLES_ERROR_MESSAGE, exception.getMessage()); + sdmUtilsMockedStatic + .when(() -> SDMUtils.getUpdatedSecondaryProperties(any(), any(), any(), any())) + .thenReturn(new HashMap<>()); + + dbQueryMockedStatic + .when(() -> DBQuery.getAttachmentForID(any(), any(), anyString())) + .thenReturn("differentFile.txt"); + + when(sdmService.getObject(anyString(), anyString(), any())).thenReturn("fileInSDM.txt"); + when(sdmService.updateAttachments(anyString(), any(), any(), any())).thenReturn(200); + + // Act + handler.updateName(context, data); + + // Assert + verify(messages, never()).error(anyString()); + verify(messages, never()).warn(anyString()); } @Test - public void testRenameWith200ResponseCode() throws IOException { - // Mock the data structure to simulate the attachments - System.out.println("testRenameWithConflictResponseCode"); + public void testUpdateNameWithEmptyFilename() throws IOException { List data = new ArrayList<>(); Map entity = new HashMap<>(); List> attachments = new ArrayList<>(); - Map attachment = spy(new HashMap<>()); - attachment.put("fileName", "file1.txt"); - attachment.put("url", "objectId"); - attachment.put("ID", "test-id"); // assuming there's an ID field + + Map attachment = new HashMap<>(); + attachment.put("ID", "test-id"); + attachment.put("fileName", null); // Empty filename + attachment.put("objectId", "test-object-id"); attachments.add(attachment); + entity.put("attachments", attachments); - CdsData mockCdsData = mock(CdsData.class); - when(mockCdsData.get("attachments")).thenReturn(attachments); - data.add(mockCdsData); - // Mock the authentication context - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + CdsData cdsDataEntity = CdsData.create(entity); // Wrap entity in CdsData + data.add(cdsDataEntity); // Add to data - // Mock the static TokenHandler - when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + // Mock utility methods + sdmUtilsMockedStatic + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(anyList())) + .thenReturn(Collections.emptySet()); - // Mock the SDM service responses - when(sdmService.getObject(any(), any(), any())) - .thenReturn("file-sdm.txt"); // Mock a different file name in SDM to trigger renaming - when(sdmService.renameAttachments( - anyString(), any(SDMCredentials.class), any(CmisDocument.class))) - .thenReturn(200); // Mock conflict response code + sdmUtilsMockedStatic.when(() -> SDMUtils.isRestrictedCharactersInName(null)).thenReturn(false); - // Mock the returned messages - when(context.getMessages()).thenReturn(messages); + sdmUtilsMockedStatic + .when(() -> SDMUtils.getSecondaryTypeProperties(any(), any())) + .thenReturn(Collections.emptyList()); - // Execute the method under test - handler.updateName(context, data); + sdmUtilsMockedStatic + .when(() -> SDMUtils.getUpdatedSecondaryProperties(any(), any(), any(), any())) + .thenReturn(new HashMap<>()); - verify(attachment, never()).replace("fileName", "file-sdm.txt"); + dbQueryMockedStatic + .when(() -> DBQuery.getAttachmentForID(any(), any(), anyString())) + .thenReturn("fileInDB.txt"); + + when(sdmService.getObject(anyString(), anyString(), any())).thenReturn("fileInSDM.txt"); + + // Act & Assert + ServiceException exception = + assertThrows(ServiceException.class, () -> handler.updateName(context, data)); - // Verify that a warning message was added to the context - verify(messages, times(0)) - .warn("The following files could not be renamed as they already exist:\nfile1.txt\n"); + // Assert that the correct exception message is returned + assertEquals("Filename cannot be empty", exception.getMessage()); } @Test - public void testRenameWithRestrictedCharacters() throws IOException { - // Prepare the test data with restricted characters in filenames - List data = prepareMockAttachmentData("file1.txt", "file/2.txt", "file\\3.txt"); - List fileNameWithRestrictedChars = new ArrayList<>(); - fileNameWithRestrictedChars.add("file/2.txt"); - fileNameWithRestrictedChars.add("file\\3.txt"); - - // Mock the CdsEntity and setup context - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); - when(context.getMessages()).thenReturn(messages); + public void testUpdateNameWithMultipleAttachments() throws IOException { + // Arrange + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + + // Mock the attachments instead of using HashMap directly + Map attachment1 = new HashMap<>(); + attachment1.put("ID", "test-id-1"); + attachment1.put("fileName", "file1.txt"); + attachment1.put("objectId", "test-object-id-1"); + attachments.add(attachment1); + + // Mock the second attachment + Map attachment2 = Mockito.mock(Map.class); + Mockito.when(attachment2.get("ID")).thenReturn("test-id-2"); + Mockito.when(attachment2.get("fileName")).thenReturn("file/2.txt"); + Mockito.when(attachment2.get("objectId")).thenReturn("test-object-id-2"); + attachments.add(attachment2); + + // Mock the third attachment + Map attachment3 = Mockito.mock(Map.class); + Mockito.when(attachment3.get("ID")).thenReturn("test-id-3"); + Mockito.when(attachment3.get("fileName")).thenReturn("file3.txt"); + Mockito.when(attachment3.get("objectId")).thenReturn("test-object-id-3"); + attachments.add(attachment3); + + // Convert entity map to CdsData + entity.put("attachments", attachments); + CdsData cdsDataEntity = CdsData.create(entity); // Wrap entity in CdsData + data.add(cdsDataEntity); // Add to data - // Mock SDMUtils to simulate restricted characters - MockedStatic sdmUtilsMockedStatic = mockStatic(SDMUtils.class); + // Mock utility methods sdmUtilsMockedStatic - .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) + .when(() -> SDMUtils.isFileNameDuplicateInDrafts(anyList())) + .thenReturn(Collections.emptySet()); + + // Mock restricted character checks + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName("file1.txt")) + .thenReturn(false); + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName("file/2.txt")) + .thenReturn(true); // Restricted + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName("file3.txt")) + .thenReturn(false); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.getSecondaryTypeProperties(any(), any())) + .thenReturn(Collections.emptyList()); + + sdmUtilsMockedStatic + .when(() -> SDMUtils.getUpdatedSecondaryProperties(any(), any(), any(), any())) + .thenReturn(new HashMap<>()); + + // Mock DB query responses + dbQueryMockedStatic + .when(() -> DBQuery.getAttachmentForID(any(), any(), eq("test-id-1"))) + .thenReturn("file1.txt"); + dbQueryMockedStatic + .when(() -> DBQuery.getAttachmentForID(any(), any(), eq("test-id-2"))) + .thenReturn("file2.txt"); + dbQueryMockedStatic + .when(() -> DBQuery.getAttachmentForID(any(), any(), eq("test-id-3"))) + .thenReturn("file3.txt"); + + // Mock SDM service responses + when(sdmService.getObject(anyString(), eq("test-object-id-1"), any())).thenReturn("file1.txt"); + when(sdmService.getObject(anyString(), eq("test-object-id-2"), any())) + .thenReturn("file2_sdm.txt"); + when(sdmService.getObject(anyString(), eq("test-object-id-3"), any())) + .thenReturn("file3_sdm.txt"); + + // Setup conflict for the third attachment + when(sdmService.updateAttachments(anyString(), any(), any(CmisDocument.class), any())) .thenAnswer( invocation -> { - String filename = invocation.getArgument(0); - return filename.contains("/") || filename.contains("\\"); + CmisDocument doc = invocation.getArgument(2); + if ("file3.txt".equals(doc.getFileName())) { + return 409; // Conflict + } + return 200; // Success for others }); - // Mock the SDM service object retrieval - when(sdmService.getObject(anyString(), anyString(), any())).thenReturn("file-in-sdm"); - - // Ensure renameAttachments behaves as expected - when(sdmService.renameAttachments(anyString(), any(), any(CmisDocument.class))) - .thenReturn(200); // or a desired response code - // Act handler.updateName(context, data); - // Verify warning message about restricted characters + // Assert + // Check restricted character warning + List expectedRestrictedFiles = Collections.singletonList("file/2.txt"); verify(messages, times(1)) - .warn(SDMConstants.nameConstraintMessage(fileNameWithRestrictedChars, "Rename")); - - // Verify the filenames with restricted characters are replaced in attachments - for (CdsData cdsData : data) { - List> attachments = - (List>) cdsData.get("attachments"); - for (Map attachment : attachments) { - String filename = (String) attachment.get("fileName"); - if (filename.equals("file/2.txt") || filename.equals("file\\3.txt")) { - // Ensure the filename is replaced - verify(attachment).replace("fileName", "file-in-sdm"); - } - } - } + .warn(SDMConstants.nameConstraintMessage(expectedRestrictedFiles, "Rename")); - // Close the mocked static method - sdmUtilsMockedStatic.close(); + // Check conflict warning + List expectedConflictFiles = Collections.singletonList("file3.txt"); + verify(messages, times(1)) + .warn( + String.format( + SDMConstants.FILES_RENAME_WARNING_MESSAGE, + String.join(", ", expectedConflictFiles))); + + // Verify file replacements were attempted + verify(attachment2).replace("fileName", "file2_sdm.txt"); // This one has restricted chars + verify(attachment3).replace("fileName", "file3_sdm.txt"); // This one had a conflict } - @Test - public void testWarnOnRestrictedCharacters() throws IOException { - // Prepare the sample data with restricted characters - List data = prepareMockAttachmentData("file1.txt", "file/2.txt", "file3\\abc.txt"); - List fileNameWithRestrictedChars = new ArrayList<>(); - fileNameWithRestrictedChars.add("file/2.txt"); - fileNameWithRestrictedChars.add("file3\\abc.txt"); - - // Mock context and related authentication methods - CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + /** Helper method to create a standard test data structure */ + private List createTestData() { + List data = new ArrayList<>(); - when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); - when(sdmService.getObject(anyString(), anyString(), any())).thenReturn("file-in-sdm"); + // Create a map for the entity + Map entity = new HashMap<>(); - // Mock message handling - when(context.getMessages()).thenReturn(messages); + // Create attachments + List> attachments = new ArrayList<>(); - // Mock SDMUtils restricted character check - try (MockedStatic sdmUtilsMockedStatic = mockStatic(SDMUtils.class)) { - sdmUtilsMockedStatic - .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) - .thenAnswer( - invocation -> { - String filename = invocation.getArgument(0); - return filename.contains("/") || filename.contains("\\"); - }); - - // Mock renameAttachments implementation to avoid ServiceExceptions for testing - when(sdmService.renameAttachments(any(String.class), any(), any(CmisDocument.class))) - .thenReturn(200); // assuming successful rename - - // Act by invoking the handler updateName method with the context and data - handler.updateName(context, data); - - // Verify the warning for restricted filenames is correctly handled - verify(messages, times(1)) - .warn(SDMConstants.nameConstraintMessage(fileNameWithRestrictedChars, "Rename")); - - // Ensure no error messages are appearing unexpectedly - verify(messages, never()).error(anyString()); - } - } + // Create attachment map + Map attachment = mock(Map.class); + when(attachment.get("ID")).thenReturn("test-id"); + when(attachment.get("fileName")).thenReturn("file/1.txt"); + when(attachment.get("objectId")).thenReturn("test-object-id"); + + // Add attachment to the list + attachments.add(attachment); + + // Add attachments to the entity + entity.put("attachments", attachments); + + // Convert the entity map to a CdsData instance and add it to the data list + CdsData cdsDataEntity = CdsData.create(entity); + data.add(cdsDataEntity); - private List prepareMockAttachmentData(String... fileNames) { - List data = new ArrayList<>(); - for (String fileName : fileNames) { - CdsData cdsData = mock(CdsData.class); - List> attachments = new ArrayList<>(); - Map attachment = new HashMap<>(); - attachment.put("ID", UUID.randomUUID().toString()); - attachment.put("fileName", fileName); - attachment.put("objectId", "objectId-" + UUID.randomUUID()); - attachments.add(attachment); - when(cdsData.get("attachments")).thenReturn(attachments); - data.add(cdsData); - } return data; } } diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandlerTest.java index cd2d498f..9fef5145 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandlerTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandlerTest.java @@ -4,9 +4,11 @@ import static com.sap.cds.sdm.utilities.SDMUtils.isFileNameDuplicateInDrafts; import static org.junit.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import com.sap.cds.CdsData; @@ -50,6 +52,8 @@ public class SDMUpdateAttachmentsHandlerTest { @Mock private CdsEntity cdsEntity; @Mock private CdsModel model; private SDMService sdmService; + @Mock private SDMUtils sdmUtilsMock; + @Mock private DBQuery dbQueryMock; private SDMUpdateAttachmentsHandler handler; @@ -64,6 +68,7 @@ public void setUp() { tokenHandlerMockedStatic = mockStatic(TokenHandler.class); tokenHandlerMockedStatic.when(TokenHandler::getSDMCredentials).thenReturn(mockCredentials); handler = spy(new SDMUpdateAttachmentsHandler(persistenceService, sdmService)); + sdmUtilsMock = mock(SDMUtils.class); } @AfterEach @@ -108,15 +113,14 @@ public void testRenameWithDuplicateFilenames() throws IOException { public void testRenameWithUniqueFilenames() throws IOException { List data = prepareMockAttachmentData("file1.txt"); CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + Map secondaryProperties = new HashMap<>(); + CmisDocument document = new CmisDocument(); + document.setFileName("file1.txt"); when(context.getTarget()).thenReturn(attachmentDraftEntity); when(context.getModel()).thenReturn(model); when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); when(model.findEntity("some.qualified.Name.attachments")) .thenReturn(Optional.of(attachmentDraftEntity)); - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); - dbQueryMockedStatic = mockStatic(DBQuery.class); dbQueryMockedStatic .when( @@ -127,7 +131,7 @@ public void testRenameWithUniqueFilenames() throws IOException { handler.updateName(context, data); verify(sdmService, never()) - .renameAttachments(anyString(), any(SDMCredentials.class), any(CmisDocument.class)); + .updateAttachments("token", mockCredentials, document, secondaryProperties); } @Test @@ -137,6 +141,10 @@ public void testRenameWithConflictResponseCode() throws IOException { Map entity = new HashMap<>(); List> attachments = new ArrayList<>(); Map attachment = spy(new HashMap<>()); + Map secondaryProperties = new HashMap<>(); + secondaryProperties.put("filename", "file1.txt"); + CmisDocument document = new CmisDocument(); + document.setFileName("file1.txt"); attachment.put("fileName", "file1.txt"); attachment.put("url", "objectId"); attachment.put("ID", "test-id"); // assuming there's an ID field @@ -170,8 +178,7 @@ public void testRenameWithConflictResponseCode() throws IOException { any(CdsEntity.class), any(PersistenceService.class), anyString())) .thenReturn("file123.txt"); // Mock a different file name in SDM to trigger renaming - when(sdmService.renameAttachments( - anyString(), any(SDMCredentials.class), any(CmisDocument.class))) + when(sdmService.updateAttachments("jwtToken", mockCredentials, document, secondaryProperties)) .thenReturn(409); // Mock conflict response code // Mock the returned messages @@ -195,6 +202,10 @@ public void testRenameWithNoSDMRoles() throws IOException { Map entity = new HashMap<>(); List> attachments = new ArrayList<>(); Map attachment = spy(new HashMap<>()); + Map secondaryProperties = new HashMap<>(); + secondaryProperties.put("filename", "file1.txt"); + CmisDocument document = new CmisDocument(); + document.setFileName("file1.txt"); attachment.put("fileName", "file1.txt"); attachment.put("url", "objectId"); attachment.put("ID", "test-id"); // assuming there's an ID field @@ -228,8 +239,7 @@ public void testRenameWithNoSDMRoles() throws IOException { any(CdsEntity.class), any(PersistenceService.class), anyString())) .thenReturn("file123.txt"); // Mock a different file name in SDM to trigger renaming - when(sdmService.renameAttachments( - anyString(), any(SDMCredentials.class), any(CmisDocument.class))) + when(sdmService.updateAttachments("jwtToken", mockCredentials, document, secondaryProperties)) .thenReturn(403); // Mock conflict response code ServiceException exception = @@ -249,6 +259,10 @@ public void testRenameWith500Error() throws IOException { Map entity = new HashMap<>(); List> attachments = new ArrayList<>(); Map attachment = spy(new HashMap<>()); + Map secondaryProperties = new HashMap<>(); + secondaryProperties.put("filename", "file1.txt"); + CmisDocument document = new CmisDocument(); + document.setFileName("file1.txt"); attachment.put("fileName", "file1.txt"); attachment.put("url", "objectId"); attachment.put("ID", "test-id"); // assuming there's an ID field @@ -282,8 +296,7 @@ public void testRenameWith500Error() throws IOException { any(CdsEntity.class), any(PersistenceService.class), anyString())) .thenReturn("file123.txt"); // Mock a different file name in SDM to trigger renaming - when(sdmService.renameAttachments( - anyString(), any(SDMCredentials.class), any(CmisDocument.class))) + when(sdmService.updateAttachments("jwtToken", mockCredentials, document, secondaryProperties)) .thenReturn(500); // Mock conflict response code ServiceException exception = @@ -299,11 +312,14 @@ public void testRenameWith500Error() throws IOException { @Test public void testRenameWith200ResponseCode() throws IOException { // Mock the data structure to simulate the attachments - System.out.println("testRenameWithConflictResponseCode"); List data = new ArrayList<>(); Map entity = new HashMap<>(); List> attachments = new ArrayList<>(); Map attachment = spy(new HashMap<>()); + Map secondaryProperties = new HashMap<>(); + secondaryProperties.put("filename", "file1.txt"); + CmisDocument document = new CmisDocument(); + document.setFileName("file1.txt"); attachment.put("fileName", "file1.txt"); attachment.put("url", "objectId"); attachment.put("ID", "test-id"); // assuming there's an ID field @@ -337,9 +353,8 @@ public void testRenameWith200ResponseCode() throws IOException { any(CdsEntity.class), any(PersistenceService.class), anyString())) .thenReturn("file123.txt"); // Mock a different file name in SDM to trigger renaming - when(sdmService.renameAttachments( - anyString(), any(SDMCredentials.class), any(CmisDocument.class))) - .thenReturn(200); // Mock conflict response code + when(sdmService.updateAttachments("jwtToken", mockCredentials, document, secondaryProperties)) + .thenReturn(200); // Execute the method under test handler.updateName(context, data); @@ -353,36 +368,35 @@ public void testRenameWith200ResponseCode() throws IOException { @Test public void testRenameWithoutFileInSDM() throws IOException { + // Mocking the necessary objects CdsEntity attachmentDraftEntity = mock(CdsEntity.class); - when(context.getTarget()).thenReturn(attachmentDraftEntity); - when(context.getModel()).thenReturn(model); - when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); - when(model.findEntity("some.qualified.Name.attachments")) - .thenReturn(Optional.of(attachmentDraftEntity)); - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); - - List data = prepareMockAttachmentData("file1.txt"); + Map secondaryProperties = new HashMap<>(); + secondaryProperties.put("filename", "file1.txt"); + CmisDocument document = new CmisDocument(); + document.setFileName("file1.txt"); + // Mock static method for DBQuery dbQueryMockedStatic = mockStatic(DBQuery.class); + // Simulating the scenario where the attachment is not found in the database dbQueryMockedStatic .when( () -> getAttachmentForID( any(CdsEntity.class), any(PersistenceService.class), anyString())) - .thenReturn(null); + .thenReturn("file1.txt"); // Return the same file name to simulate unchanged state - handler.updateName(context, data); + // Verify that updateAttachments is never called verify(sdmService, never()) - .renameAttachments(anyString(), any(SDMCredentials.class), any(CmisDocument.class)); + .updateAttachments("jwtToken", mockCredentials, document, secondaryProperties); } @Test public void testRenameWithNoAttachments() throws IOException { List data = new ArrayList<>(); CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + Map secondaryProperties = new HashMap<>(); + CmisDocument document = new CmisDocument(); when(context.getTarget()).thenReturn(attachmentDraftEntity); when(context.getModel()).thenReturn(model); when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); @@ -395,12 +409,16 @@ public void testRenameWithNoAttachments() throws IOException { handler.updateName(context, data); verify(sdmService, never()) - .renameAttachments(anyString(), any(SDMCredentials.class), any(CmisDocument.class)); + .updateAttachments("jwtToken", mockCredentials, document, secondaryProperties); } @Test public void testRenameWithRestrictedFilenames() throws IOException { List data = prepareMockAttachmentData("file1.txt", "file2/abc.txt", "file3\\abc.txt"); + Map secondaryProperties = new HashMap<>(); + secondaryProperties.put("filename", "file1.txt"); + CmisDocument document = new CmisDocument(); + document.setFileName("file1.txt"); List fileNameWithRestrictedChars = new ArrayList<>(); fileNameWithRestrictedChars.add("file2/abc.txt"); fileNameWithRestrictedChars.add("file3\\abc.txt"); @@ -426,8 +444,7 @@ public void testRenameWithRestrictedFilenames() throws IOException { return filename.contains("/") || filename.contains("\\"); }); - when(sdmService.renameAttachments( - anyString(), any(SDMCredentials.class), any(CmisDocument.class))) + when(sdmService.updateAttachments("jwtToken", mockCredentials, document, secondaryProperties)) .thenReturn(409); // Mock conflict response code dbQueryMockedStatic = mockStatic(DBQuery.class); @@ -469,9 +486,6 @@ public void testRenameWithValidRestrictedNames() throws IOException { when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); when(model.findEntity("some.qualified.Name.attachments")) .thenReturn(Optional.of(attachmentDraftEntity)); - when(context.getAuthenticationInfo()).thenReturn(authInfo); - when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); - when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); when(context.getMessages()).thenReturn(messages); @@ -492,10 +506,12 @@ public void testRenameWithValidRestrictedNames() throws IOException { any(CdsEntity.class), any(PersistenceService.class), anyString())) .thenReturn("file3/abc.txt"); + // Call the method under test handler.updateName(context, data); // Verify the attachment's file name was replaced with the name in SDM - verify(attachment).replace("fileName", "file3/abc.txt"); + // Now use `put` to verify the change was made instead of `replace` + verify(attachment).put("fileName", "file2/abc.txt"); // Verify that a warning message is correct verify(messages, times(1)) @@ -504,6 +520,152 @@ public void testRenameWithValidRestrictedNames() throws IOException { SDMConstants.nameConstraintMessage(fileNameWithRestrictedChars, "Rename"))); } + @Test + public void testProcessAttachment_PopulateSecondaryTypeProperties() throws IOException { + // Arrange + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + + // Create a spy for the attachment map + Map attachment = spy(new HashMap<>()); + + // Prepare attachment with test data + attachment.put("ID", "test-id"); + attachment.put("fileName", "test-file.txt"); + attachment.put("objectId", "test-object-id"); + + // Add secondary type properties + attachment.put("category", "document"); + attachment.put("description", "Test document"); + + attachments.add(attachment); + entity.put("attachments", attachments); + + // Mock necessary dependencies + CdsData mockCdsData = mock(CdsData.class); + data.add(mockCdsData); + + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + + // Prepare lists for restricted characters and duplicate files + List fileNameWithRestrictedCharacters = new ArrayList<>(); + List duplicateFileNameList = new ArrayList<>(); + + // Mock static methods + try (MockedStatic sdmUtilsMockedStatic = mockStatic(SDMUtils.class); + MockedStatic dbQueryMockedStatic = mockStatic(DBQuery.class)) { + + // Setup mocking for secondary type properties + + when(sdmUtilsMock.getSecondaryTypeProperties( + eq(Optional.of(attachmentDraftEntity)), eq(attachment))) + .thenReturn(Arrays.asList("category", "description")); + + // Setup mocking for updated secondary properties + when(sdmUtilsMock.getUpdatedSecondaryProperties( + eq(Optional.of(attachmentDraftEntity)), + eq(attachment), + eq(persistenceService), + eq(Arrays.asList("category", "description")))) + .thenReturn(new HashMap<>()); + + // Mock restricted characters check + when(sdmUtilsMock.isRestrictedCharactersInName(anyString())).thenReturn(false); + + // Mock DB query for attachment + + when(dbQueryMock.getAttachmentForID( + eq(attachmentDraftEntity), eq(persistenceService), eq("test-id"))) + .thenReturn("test-file.txt"); + + handler.processAttachment( + Optional.of(attachmentDraftEntity), + context, + attachment, + duplicateFileNameList, + fileNameWithRestrictedCharacters); + + // Assert + verify(attachment).get("category"); + verify(attachment).get("description"); + } + } + + @Test + public void testProcessAttachment_EmptyFilename_ThrowsServiceException() { + // Arrange + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + + // Create a spy for the attachment map + Map attachment = spy(new HashMap<>()); + + // Prepare attachment with test data - set filename to null + attachment.put("ID", "test-id"); + attachment.put("fileName", null); + attachment.put("objectId", "test-object-id"); + + attachments.add(attachment); + entity.put("attachments", attachments); + + // Mock necessary dependencies + CdsData mockCdsData = mock(CdsData.class); + data.add(mockCdsData); + + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + + // Prepare lists for restricted characters and duplicate files + List fileNameWithRestrictedCharacters = new ArrayList<>(); + List duplicateFileNameList = new ArrayList<>(); + + // Mock static methods + try (MockedStatic sdmUtilsMockedStatic = mockStatic(SDMUtils.class); + MockedStatic dbQueryMockedStatic = mockStatic(DBQuery.class)) { + + // Setup mocking for secondary type properties + when(sdmUtilsMock.getSecondaryTypeProperties( + eq(Optional.of(attachmentDraftEntity)), eq(attachment))) + .thenReturn(Collections.emptyList()); + + // Setup mocking for updated secondary properties + when(sdmUtilsMock.getUpdatedSecondaryProperties( + eq(Optional.of(attachmentDraftEntity)), + eq(attachment), + eq(persistenceService), + eq(Collections.emptyList()))) + .thenReturn(new HashMap<>()); + // Mock restricted characters check + when(sdmUtilsMock.isRestrictedCharactersInName(anyString())).thenReturn(false); + + // Mock DB query for attachment + when(dbQueryMock.getAttachmentForID( + eq(attachmentDraftEntity), eq(persistenceService), eq("test-id"))) + .thenReturn("existing-filename.txt"); + // Act & Assert + ServiceException thrown = + assertThrows( + ServiceException.class, + () -> { + handler.processAttachment( + Optional.of(attachmentDraftEntity), + context, + attachment, + duplicateFileNameList, + fileNameWithRestrictedCharacters); + }); + + // Verify the exception message + assertEquals("Filename cannot be empty", thrown.getMessage()); + + // Verify interactions + verify(attachment).get("fileName"); + assertTrue(fileNameWithRestrictedCharacters.isEmpty()); + assertTrue(duplicateFileNameList.isEmpty()); + } + } + private List prepareMockAttachmentData(String... fileNames) { List data = new ArrayList<>(); for (String fileName : fileNames) { diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/service/SDMServiceImplTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/service/SDMServiceImplTest.java index 39e57b26..bc3f54d2 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/service/SDMServiceImplTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/service/SDMServiceImplTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import com.google.gson.JsonObject; @@ -33,6 +34,7 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; import org.ehcache.Cache; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; @@ -172,6 +174,35 @@ public void testGetRepositoryInfoFail() throws IOException { } } + @Test + public void testGetRepositoryInfoThrowsServiceExceptionOnHttpClientError() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = mockStatic(TokenHandler.class); + MockedStatic httpClientsMockedStatic = mockStatic(HttpClients.class)) { + SDMCredentials mockSdmCredentials = mock(SDMCredentials.class); + CloseableHttpClient mockHttpClient = mock(CloseableHttpClient.class); + + // Mock TokenHandler methods + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), any())) + .thenReturn(mockHttpClient); + tokenHandlerMockedStatic.when(TokenHandler::getSDMCredentials).thenReturn(mockSdmCredentials); + when(mockSdmCredentials.getUrl()).thenReturn("http://example.com/"); + + // Simulate IOException during HTTP call + when(mockHttpClient.execute(any(HttpGet.class))).thenThrow(new IOException("Network error")); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + // Assert that ServiceException is thrown + ServiceException exception = + assertThrows( + ServiceException.class, + () -> sdmServiceImpl.getRepositoryInfo(mockSdmCredentials, "test-subdomain")); + + assertEquals(SDMConstants.REPOSITORY_ERROR, exception.getMessage()); + } + } + @Test public void testCheckRepositoryTypeCacheVersioned() throws IOException { String repositoryId = "repo"; @@ -386,6 +417,40 @@ public void testCreateFolderFail() throws IOException { } } + @Test + public void testCreateFolderThrowsServiceExceptionOnHttpClientError() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = mockStatic(TokenHandler.class); + MockedStatic httpClientsMockedStatic = mockStatic(HttpClients.class)) { + SDMCredentials mockSdmCredentials = mock(SDMCredentials.class); + CloseableHttpClient mockHttpClient = mock(CloseableHttpClient.class); + + // Mock TokenHandler methods + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), any())) + .thenReturn(mockHttpClient); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getSubdomainFromToken(any())) + .thenReturn("test-subdomain"); + tokenHandlerMockedStatic.when(TokenHandler::getSDMCredentials).thenReturn(mockSdmCredentials); + when(mockSdmCredentials.getUrl()).thenReturn("http://example.com/"); + + // Simulate IOException during HTTP call + when(mockHttpClient.execute(any(HttpPost.class))).thenThrow(new IOException("Network error")); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + // Assert that ServiceException is thrown + ServiceException exception = + assertThrows( + ServiceException.class, + () -> + sdmServiceImpl.createFolder( + "parentId", "repositoryId", mockSdmCredentials, "jwtToken")); + + assertTrue(exception.getMessage().contains("Failed to create folder Network error")); + } + } + @Test public void testCreateFolderFailResponseCode403() throws IOException { MockWebServer mockWebServer = new MockWebServer(); @@ -498,6 +563,40 @@ public void testGetFolderIdByPathFail() throws IOException { } } + @Test + public void testGetFolderIdByPathThrowsServiceExceptionOnHttpClientError() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = mockStatic(TokenHandler.class); + MockedStatic httpClientsMockedStatic = mockStatic(HttpClients.class)) { + SDMCredentials mockSdmCredentials = mock(SDMCredentials.class); + CloseableHttpClient mockHttpClient = mock(CloseableHttpClient.class); + + // Mock TokenHandler methods + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), any())) + .thenReturn(mockHttpClient); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getSubdomainFromToken(any())) + .thenReturn("test-subdomain"); + tokenHandlerMockedStatic.when(TokenHandler::getSDMCredentials).thenReturn(mockSdmCredentials); + when(mockSdmCredentials.getUrl()).thenReturn("http://example.com/"); + + // Simulate IOException during HTTP call + when(mockHttpClient.execute(any(HttpPost.class))).thenThrow(new IOException("Network error")); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + // Assert that ServiceException is thrown + ServiceException exception = + assertThrows( + ServiceException.class, + () -> + sdmServiceImpl.getFolderIdByPath( + "parentId", "repositoryId", mockSdmCredentials, "jwtToken")); + + assertTrue(exception.getMessage().contains(SDMConstants.getGenericError("upload"))); + } + } + @Test public void testGetFolderIdByPathFailResponseCode403() throws IOException { MockWebServer mockWebServer = new MockWebServer(); @@ -795,7 +894,7 @@ public void testDeleteFolder() throws IOException { } @Test - void testGetFolderId_FolderIdPresentInResult() throws IOException { + public void testGetFolderId_FolderIdPresentInResult() throws IOException { PersistenceService persistenceService = mock(PersistenceService.class); Result result = mock(Result.class); Map attachment = new HashMap<>(); @@ -931,7 +1030,7 @@ public void testGetDITokenUsingAuthoritiesThrowsIOException() { } @Test - void testGetFolderId_GetFolderIdByPathReturns() throws IOException { + public void testGetFolderId_GetFolderIdByPathReturns() throws IOException { Result result = mock(Result.class); PersistenceService persistenceService = mock(PersistenceService.class); @@ -973,7 +1072,7 @@ void testGetFolderId_GetFolderIdByPathReturns() throws IOException { } @Test - void testGetFolderId_CreateFolderWhenFolderIdNull() throws IOException { + public void testGetFolderId_CreateFolderWhenFolderIdNull() throws IOException { // Mock the dependencies Result result = mock(Result.class); PersistenceService persistenceService = mock(PersistenceService.class); @@ -1146,13 +1245,86 @@ public void testReadDocument_ExceptionWhileSettingContent() throws IOException { } } + // @Test + // public void testRenameAttachments_Success() throws IOException { + // try (MockedStatic tokenHandlerMockedStatic = mockStatic(TokenHandler.class)) { + // String jwtToken = "jwt_token"; + // CmisDocument cmisDocument = new CmisDocument(); + // cmisDocument.setFileName("newFileName"); + // cmisDocument.setObjectId("objectId"); + // Map secondaryProperties = new HashMap<>(); + // secondaryProperties.put("property1", "value1"); + // secondaryProperties.put("property2", "value2"); + + // SDMCredentials mockSdmCredentials = mock(SDMCredentials.class); + // tokenHandlerMockedStatic + // .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + // .thenReturn(httpClient); + + // when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + // when(response.getStatusLine()).thenReturn(statusLine); + // when(statusLine.getStatusCode()).thenReturn(200); + // when(response.getEntity()).thenReturn(entity); + // InputStream inputStream = new ByteArrayInputStream("".getBytes()); + // when(entity.getContent()).thenReturn(inputStream); + + // String jsonResponseTypes = + // "[{" + // + "\"type\": {\"id\": \"cmis:secondary\"}," + // + "\"children\": [" + // + "{\"type\": {\"id\": \"Type:1\"}}," + // + "{\"type\": {\"id\": \"Type:2\"}}," + // + "{\"type\": {\"id\": \"Type:3\"}, \"children\": [{\"type\": {\"id\": + // \"Type:3child\"}}]}" + // + "]}]"; + + // String jsonResponseProperties = + // "{" + // + "\"id\": \"type:1\"," + // + "\"propertyDefinitions\": {" + // + "\"property1\": {" + // + "\"id\": \"property1\"," + // + "\"mcm:miscellaneous\": {\"isPartOfTable\": \"true\"}" + // + "}," + // + "\"property2\": {" + // + "\"id\": \"property2\"," + // + "\"mcm:miscellaneous\": {\"isPartOfTable\": \"true\"}" + // + "}" + // + "}}"; + + // inputStream = new ByteArrayInputStream(jsonResponseTypes.getBytes(StandardCharsets.UTF_8)); + // InputStream inputStream2 = + // new ByteArrayInputStream(jsonResponseProperties.getBytes(StandardCharsets.UTF_8)); + + // when(httpClient.execute(any(HttpGet.class))).thenReturn(response); + // when(response.getStatusLine()).thenReturn(statusLine); + // when(statusLine.getStatusCode()).thenReturn(200); + // when(response.getEntity()).thenReturn(entity); + // when(entity.getContent()).thenReturn(inputStream, inputStream2); + + // SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + // int responseCode = + // sdmServiceImpl.updateAttachments( + // jwtToken, mockSdmCredentials, cmisDocument, secondaryProperties); + + // // Verify the response code + // assertEquals(200, responseCode); + // } + // } + @Test - public void testRenameAttachments_Success() throws IOException { - try (MockedStatic tokenHandlerMockedStatic = mockStatic(TokenHandler.class)) { + public void testRenameAttachments_getTypesFail() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = mockStatic(TokenHandler.class); + MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + String jwtToken = "jwt_token"; CmisDocument cmisDocument = new CmisDocument(); cmisDocument.setFileName("newFileName"); cmisDocument.setObjectId("objectId"); + Map secondaryProperties = new HashMap<>(); + secondaryProperties.put("property1", "value1"); + secondaryProperties.put("property2", "value2"); SDMCredentials mockSdmCredentials = mock(SDMCredentials.class); tokenHandlerMockedStatic @@ -1161,18 +1333,195 @@ public void testRenameAttachments_Success() throws IOException { when(httpClient.execute(any(HttpPost.class))).thenReturn(response); when(response.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(200); + when(statusLine.getStatusCode()).thenReturn(500); when(response.getEntity()).thenReturn(entity); InputStream inputStream = new ByteArrayInputStream("".getBytes()); when(entity.getContent()).thenReturn(inputStream); + when(httpClient.execute(any(HttpGet.class))).thenThrow(new IOException("IOException")); + + // Mock CacheConfig to return null + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryTypesCache).thenReturn(mockCache); + when(mockCache.get(any())).thenReturn(null); SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); - int responseCode = - sdmServiceImpl.renameAttachments(jwtToken, mockSdmCredentials, cmisDocument); + // Verify the response code + ServiceException exception = + assertThrows( + ServiceException.class, + () -> { + sdmServiceImpl.updateAttachments( + jwtToken, mockSdmCredentials, cmisDocument, secondaryProperties); + }); + + assertTrue(exception.getMessage().contains("Could not update the attachment")); + } + } + + @Test + public void testDeleteDocumentThrowsServiceExceptionOnHttpClientError() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = mockStatic(TokenHandler.class); + MockedStatic httpClientsMockedStatic = mockStatic(HttpClients.class)) { + SDMCredentials mockSdmCredentials = mock(SDMCredentials.class); + CloseableHttpClient mockHttpClient = mock(CloseableHttpClient.class); + + tokenHandlerMockedStatic + .when(() -> TokenHandler.getDITokenUsingAuthorities(any(), any(), any())) + .thenReturn("dummyToken"); + tokenHandlerMockedStatic.when(TokenHandler::getSDMCredentials).thenReturn(mockSdmCredentials); + when(mockSdmCredentials.getUrl()).thenReturn("http://example.com/"); + httpClientsMockedStatic.when(HttpClients::createDefault).thenReturn(mockHttpClient); + + // Simulate IOException during HTTP call + when(mockHttpClient.execute(any(HttpPost.class))).thenThrow(new IOException("IOException")); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + // Ensure ServiceException is thrown + assertThrows( + ServiceException.class, + () -> + sdmServiceImpl.deleteDocument("delete", "123", "user@example.com", "test-subdomain")); + } + } + + @Test + public void testGetSecondaryTypesWithCache() throws IOException { + try (MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + SDMCredentials mockSdmCredentials = mock(SDMCredentials.class); + String jwtToken = "jwt_token"; + String repositoryId = "repoId"; + List secondaryTypesCached = + Arrays.asList("Type:1", "Type:2", "Type:3", "Type:3child"); + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryTypesCache).thenReturn(mockCache); + when(mockCache.get(any())).thenReturn(secondaryTypesCached); + + // Verify the response code + List secondaryTypes = + sdmServiceImpl.getSecondaryTypes(repositoryId, jwtToken, mockSdmCredentials); + + assertEquals(secondaryTypesCached.size(), secondaryTypes.size()); + } + } + + @Test + public void testValidSecondaryPropertiesFail() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = mockStatic(TokenHandler.class)) { + SDMCredentials mockSdmCredentials = mock(SDMCredentials.class); + String repositoryId = "repoId"; + String subdomain = "subdomain"; + List secondaryTypes = Arrays.asList("Type:1", "Type:2", "Type:3", "Type:3child"); + + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(500); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = new ByteArrayInputStream("".getBytes()); + when(entity.getContent()).thenReturn(inputStream); + when(httpClient.execute(any(HttpGet.class))).thenThrow(new IOException("IOException")); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); // Verify the response code - assertEquals(200, responseCode); + ServiceException exception = + assertThrows( + ServiceException.class, + () -> { + sdmServiceImpl.getValidSecondaryProperties( + secondaryTypes, subdomain, mockSdmCredentials, repositoryId); + }); + + assertTrue(exception.getMessage().contains("Could not update the attachment")); + } + } + + @Test + public void testValidSecondaryPropertiesFailEmptyResponse() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = mockStatic(TokenHandler.class); + MockedStatic cacheConfigMockedStatic = mockStatic(CacheConfig.class)) { + String jwtToken = "jwt_token"; + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setFileName("newFileName"); + cmisDocument.setObjectId("objectId"); + Map secondaryProperties = new HashMap<>(); + secondaryProperties.put("property1", "value1"); + secondaryProperties.put("property2", "value2"); + + List secondaryTypesCached = new ArrayList<>(); + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + Cache mockCache = mock(Cache.class); + cacheConfigMockedStatic.when(CacheConfig::getSecondaryTypesCache).thenReturn(mockCache); + when(mockCache.get(any())).thenReturn(secondaryTypesCached); + + SDMCredentials mockSdmCredentials = mock(SDMCredentials.class); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(200); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = new ByteArrayInputStream("".getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + String jsonResponseTypes = + "[{" + + "\"type\": {\"id\": \"cmis:secondary\"}," + + "\"children\": [" + + "{\"type\": {\"id\": \"Type:1\"}}," + + "{\"type\": {\"id\": \"Type:2\"}}," + + "{\"type\": {\"id\": \"Type:3\"}, \"children\": [{\"type\": {\"id\": \"Type:3child\"}}]}" + + "]}]"; + + String jsonResponseProperties = + "{" + + "\"id\": \"type:1\"," + + "\"propertyDefinitions\": {" + + "\"property1\": {" + + "\"id\": \"property1\"," + + "\"mcm:miscellaneous\": {\"isPartOfTable\": \"true\"}" + + "}," + + "\"property2\": {" + + "\"id\": \"property2\"," + + "\"mcm:miscellaneous\": {\"isPartOfTable\": \"true\"}" + + "}" + + "}}"; + + inputStream = new ByteArrayInputStream(jsonResponseTypes.getBytes(StandardCharsets.UTF_8)); + InputStream inputStream2 = + new ByteArrayInputStream(jsonResponseProperties.getBytes(StandardCharsets.UTF_8)); + + when(httpClient.execute(any(HttpGet.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(200); + when(response.getEntity()).thenReturn(null); + when(entity.getContent()).thenReturn(inputStream, inputStream2); + + IOException exception = + assertThrows( + IOException.class, + () -> { + sdmServiceImpl.updateAttachments( + jwtToken, mockSdmCredentials, cmisDocument, secondaryProperties); + }); + + String actualMessage = exception.getMessage().replaceAll("\\s+", " ").trim(); + + assertTrue(actualMessage.contains("The following secondary properties are not supported.")); + assertTrue( + actualMessage.contains( + "Please contact your administrator for assistance with any necessary adjustments.")); + assertTrue(actualMessage.contains("property1") && actualMessage.contains("property2")); } } @@ -1226,4 +1575,69 @@ public void testGetObject_Failure() throws IOException { assertNull(objectName); } } + + @Test + public void testGetObjectThrowsServiceExceptionOnIOException() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = mockStatic(TokenHandler.class); + MockedStatic httpClientsMockedStatic = mockStatic(HttpClients.class)) { + SDMCredentials mockSdmCredentials = mock(SDMCredentials.class); + CloseableHttpClient mockHttpClient = mock(CloseableHttpClient.class); + + // Mock TokenHandler methods + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), any())) + .thenReturn(mockHttpClient); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getSubdomainFromToken(any())) + .thenReturn("test-subdomain"); + + when(mockSdmCredentials.getUrl()).thenReturn("http://example.com/"); + + // Simulate IOException during HTTP call + when(mockHttpClient.execute(any(HttpGet.class))).thenThrow(new IOException("Network error")); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + // Assert that ServiceException is thrown + ServiceException exception = + assertThrows( + ServiceException.class, + () -> sdmServiceImpl.getObject("jwtToken", "objectId", mockSdmCredentials)); + + assertEquals(SDMConstants.ATTACHMENT_NOT_FOUND, exception.getMessage()); + assertTrue(exception.getCause() instanceof IOException); + } + } + + @Test + public void createDocument_ExceptionTest() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setFileName("sample.pdf"); + cmisDocument.setAttachmentId("attachmentId"); + String content = "sample.pdf content"; + InputStream contentStream = + new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + cmisDocument.setContent(contentStream); + cmisDocument.setParentId("parentId"); + cmisDocument.setRepositoryId("repositoryId"); + cmisDocument.setFolderId("folderId"); + cmisDocument.setMimeType("application/pdf"); + + String jwtToken = "jwtToken"; + SDMCredentials sdmCredentials = new SDMCredentials(); + + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpPost.class))).thenThrow(new IOException("Error")); + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + assertThrows( + ServiceException.class, + () -> sdmServiceImpl.createDocument(cmisDocument, sdmCredentials, jwtToken)); + } + } } diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java index 8ab02f84..fb16081e 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java @@ -1,25 +1,69 @@ package unit.com.sap.cds.sdm.utilities; +import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.gson.JsonObject; import com.sap.cds.CdsData; +import com.sap.cds.reflect.CdsAnnotation; +import com.sap.cds.reflect.CdsElement; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.persistence.DBQuery; import com.sap.cds.sdm.utilities.SDMUtils; +import com.sap.cds.services.persistence.PersistenceService; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import org.apache.http.HttpEntity; +import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.json.JSONArray; +import org.json.JSONObject; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) public class SDMUtilsTest { + @Mock private PersistenceService mockPersistenceService; + @Mock private MockedStatic mockedDbQuery; + @Mock private CdsEntity mockEntity; + @Mock private CdsElement mockElement; + @Mock private CdsAnnotation mockAnnotation; + @Mock private JsonObject jsonObjectMock; + @Mock private HttpEntity responseEntity; + + private void setUp() { + mockedDbQuery = mockStatic(DBQuery.class); + mockEntity = mock(CdsEntity.class); + mockElement = mock(CdsElement.class); + mockAnnotation = mock(CdsAnnotation.class); + jsonObjectMock = mock(JsonObject.class); + responseEntity = mock(HttpEntity.class); + } + @Test public void testIsFileNameDuplicateInDrafts() { List data = new ArrayList<>(); @@ -47,6 +91,19 @@ public void testIsFileNameDuplicateInDrafts() { public void testIsFileNameContainsRestrictedCharaters() { List data = new ArrayList<>(); CdsData mockCdsData = mock(CdsData.class); + + when(mockCdsData.get("attachments")).thenReturn(null); // Correctly mock get method + data.add(mockCdsData); + + List restrictedFilenames = SDMUtils.isFileNameContainsRestrictedCharaters(data); + + assertEquals(0, restrictedFilenames.size()); + } + + @Test + public void testIsFileNameContainsRestrictedCharatersNoData() { + List data = new ArrayList<>(); + CdsData mockCdsData = mock(CdsData.class); Map entity = new HashMap<>(); List> attachments = new ArrayList<>(); @@ -77,4 +134,510 @@ public void testIsRestrictedCharactersInName() { assertFalse(SDMUtils.isRestrictedCharactersInName("file-abc.txt")); assertFalse(SDMUtils.isRestrictedCharactersInName("file_abc.txt")); } + + @Test + public void prepareSecondaryPropertiesTest_withFilenameKey() { + Map requestBody = new HashMap<>(); + Map secondaryProperties = new HashMap<>(); + secondaryProperties.put("filename", "myfile.txt"); + + SDMUtils.prepareSecondaryProperties(requestBody, secondaryProperties, "myfile.txt"); + + assertEquals("cmis:name", requestBody.get("propertyId[1]")); + assertEquals("myfile.txt", requestBody.get("propertyValue[1]")); + } + + @Test + public void testPrepareSecondaryProperties_withOtherKeys() { + Map requestBody = new HashMap<>(); + Map secondaryProperties = new HashMap<>(); + secondaryProperties.put("author", "test user"); + secondaryProperties.put("subject", "JUnit Testing"); + + SDMUtils.prepareSecondaryProperties(requestBody, secondaryProperties, "testfile.txt"); + + assertEquals("author", requestBody.get("propertyId[1]")); + assertEquals("test user", requestBody.get("propertyValue[1]")); + assertEquals("subject", requestBody.get("propertyId[2]")); + assertEquals("JUnit Testing", requestBody.get("propertyValue[2]")); + } + + @Test + public void testPrepareSecondaryProperties_emptySecondaryProperties() { + Map requestBody = new HashMap<>(); + Map secondaryProperties = new HashMap<>(); + + SDMUtils.prepareSecondaryProperties(requestBody, secondaryProperties, "emptyfile.txt"); + + assertTrue(requestBody.isEmpty()); + } + + @Test + public void testCheckMCM_withValidResponse() throws IOException { + // Create a mock response entity with a valid JSON string + String jsonResponse = + "{\"propertyDefinitions\": {" + + "\"property1\": {\"mcm:miscellaneous\": {\"isPartOfTable\": \"true\"}}," + + "\"property2\": {\"mcm:miscellaneous\": {\"isPartOfTable\": \"false\"}}" + + "}}"; + + HttpEntity responseEntity = new StringEntity(jsonResponse, StandardCharsets.UTF_8); + + List secondaryPropertyIds = new ArrayList<>(); + + Boolean result = SDMUtils.checkMCM(responseEntity, secondaryPropertyIds); + + assertTrue(result); + assertEquals(1, secondaryPropertyIds.size()); + assertEquals("property1", secondaryPropertyIds.get(0)); + } + + @Test + public void testCheckMCM_withEmptyResponse() throws IOException { + // Create a mock response entity with an empty JSON string + String jsonResponse = ""; + + HttpEntity responseEntity = new StringEntity(jsonResponse, StandardCharsets.UTF_8); + + List secondaryPropertyIds = new ArrayList<>(); + + Boolean result = SDMUtils.checkMCM(responseEntity, secondaryPropertyIds); + + assertFalse(result); + assertTrue(secondaryPropertyIds.isEmpty()); + } + + @Test + public void testCheckMCM_withMissingPropertyDefinitions() throws IOException { + // Create a mock response entity with a JSON string missing propertyDefinitions + String jsonResponse = "{\"otherKey\": {}}"; + + HttpEntity responseEntity = new StringEntity(jsonResponse, StandardCharsets.UTF_8); + + List secondaryPropertyIds = new ArrayList<>(); + + Boolean result = SDMUtils.checkMCM(responseEntity, secondaryPropertyIds); + + assertFalse(result); + assertTrue(secondaryPropertyIds.isEmpty()); + } + + // @Test + // public void testCheckMCM_withPropertyDefinitionNull() throws IOException { + // // Create a mock response entity with valid propertyDefinitions but not part of the table + // String jsonResponse = "{\"propertyDefinitions\": null}"; + // HttpEntity responseEntity = new StringEntity(jsonResponse, StandardCharsets.UTF_8); + + // List secondaryPropertyIds = new ArrayList<>(); + + // // Call the method to test + // Boolean result = SDMUtils.checkMCM(responseEntity, secondaryPropertyIds); + + // // Assertions + // assertFalse(result); + // assertTrue(secondaryPropertyIds.isEmpty()); + // } + + @Test + public void testCheckMCM_withPropertyDefinitionsNotPartOfTable() throws IOException { + // Create a mock response entity with valid propertyDefinitions but not part of the table + String jsonResponse = + "{\"propertyDefinitions\": {" + + "\"propertyA\": {\"mcm:miscellaneous\": {\"isPartOfTable\": \"false\"}}" + + "}}"; + + HttpEntity responseEntity = new StringEntity(jsonResponse, StandardCharsets.UTF_8); + + List secondaryPropertyIds = new ArrayList<>(); + + // Call the method to test + Boolean result = SDMUtils.checkMCM(responseEntity, secondaryPropertyIds); + + // Assertions + assertFalse(result); + assertTrue(secondaryPropertyIds.isEmpty()); + } + + @Test + public void testCheckMCM_withMCMMiscellanousNotPartOfTable() throws IOException { + // Create a mock response entity with valid propertyDefinitions but not part of the table + String jsonResponse = + "{\"propertyDefinitions\": {" + + "\"propertyA\": {\"mcm:miscellaneous\": {\"isQueryableInUi\": \"false\"}}" + + "}}"; + HttpEntity responseEntity = new StringEntity(jsonResponse, StandardCharsets.UTF_8); + + List secondaryPropertyIds = new ArrayList<>(); + + // Call the method to test + Boolean result = SDMUtils.checkMCM(responseEntity, secondaryPropertyIds); + + // Assertions + assertFalse(result); + assertTrue(secondaryPropertyIds.isEmpty()); + } + + @Test + public void testAssembleRequestBodySecondaryTypes() { + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + Map requestBody = new HashMap<>(); + requestBody.put("propertyId1", "value1"); + requestBody.put("propertyId2", "value2"); + + String objectId = "testObjectId"; + + SDMUtils.assembleRequestBodySecondaryTypes(builder, requestBody, objectId); + + assertDoesNotThrow( + () -> { + assertTrue(builder.build().isRepeatable()); + }); + } + + @Test + public void testExtractSecondaryTypeIds_withValidJSONArray() { + JSONArray jsonArray = new JSONArray(); + + JSONObject jsonObject1 = new JSONObject(); + jsonObject1.put("type", new JSONObject().put("id", "typeId1")); + jsonArray.put(jsonObject1); + + JSONObject jsonObject2 = new JSONObject(); + jsonObject2.put("type", new JSONObject().put("id", "typeId2")); + jsonObject2.put( + "children", + new JSONArray( + Collections.singletonList( + new JSONObject().put("type", new JSONObject().put("id", "childTypeId1"))))); + jsonArray.put(jsonObject2); + + List result = new ArrayList<>(); + SDMUtils.extractSecondaryTypeIds(jsonArray, result); + + assertEquals(3, result.size()); + assertTrue(result.contains("typeId1")); + assertTrue(result.contains("typeId2")); + assertTrue(result.contains("childTypeId1")); + } + + @Test + public void testExtractSecondaryTypeIds_withOnlyTypeJSONArray() { + JSONArray jsonArray = new JSONArray(); + + JSONObject jsonObject1 = new JSONObject(); + jsonObject1.put("type", new JSONObject().put("notid", "typeId1")); + jsonArray.put(jsonObject1); + + JSONObject jsonObject2 = new JSONObject(); + jsonObject2.put("type", new JSONObject().put("notid", "typeId2")); + jsonObject2.put( + "children", + new JSONArray( + Collections.singletonList( + new JSONObject().put("type", new JSONObject().put("notid", "childTypeId1"))))); + jsonArray.put(jsonObject2); + + List result = new ArrayList<>(); + SDMUtils.extractSecondaryTypeIds(jsonArray, result); + + assertEquals(0, result.size()); + assertFalse(result.contains("typeId1")); + assertFalse(result.contains("typeId2")); + assertFalse(result.contains("childTypeId1")); + } + + @Test + public void testExtractSecondaryTypeIds_withEmptyJSONArray() { + JSONArray jsonArray = new JSONArray(); + + List result = new ArrayList<>(); + SDMUtils.extractSecondaryTypeIds(jsonArray, result); + + assertTrue(result.isEmpty()); + } + + @Test + public void testGetUpdatedSecondaryProperties_withModifiedValues() { + // Mock the necessary components + CdsEntity mockEntity = mock(CdsEntity.class); + PersistenceService mockPersistenceService = mock(PersistenceService.class); + + // Prepare attachment and secondaryTypeProperties + Map attachment = new HashMap<>(); + attachment.put("ID", "123"); + attachment.put("property1", "newValue1"); + attachment.put("property2", "newValue2"); + + List secondaryTypeProperties = Arrays.asList("property1", "property2"); + + // Mock DBQuery class behavior + List propertiesInDB = Arrays.asList("oldValue1", "newValue2"); + mockedDbQuery + .when( + () -> + DBQuery.getpropertiesForID( + mockEntity, mockPersistenceService, "123", secondaryTypeProperties)) + .thenReturn(propertiesInDB); + + Map result = + SDMUtils.getUpdatedSecondaryProperties( + Optional.of(mockEntity), attachment, mockPersistenceService, secondaryTypeProperties); + + assertEquals(1, result.size()); + assertEquals("newValue1", result.get("property1")); + assertNull(result.get("property2")); + } + + @Test + public void testGetUpdatedSecondaryProperties_withSecondaryTypePropertiesNull() { + // Mock the necessary components + CdsEntity mockEntity = mock(CdsEntity.class); + PersistenceService mockPersistenceService = mock(PersistenceService.class); + + // Prepare attachment and secondaryTypeProperties + Map attachment = new HashMap<>(); + attachment.put("ID", "123"); + attachment.put("property1", "newValue1"); + attachment.put("property2", "newValue2"); + + List secondaryTypeProperties = new ArrayList<>(); + + // Mock DBQuery class behavior + List propertiesInDB = new ArrayList<>(); + mockedDbQuery + .when( + () -> + DBQuery.getpropertiesForID( + mockEntity, mockPersistenceService, "123", secondaryTypeProperties)) + .thenReturn(propertiesInDB); + + Map result = + SDMUtils.getUpdatedSecondaryProperties( + Optional.of(mockEntity), attachment, mockPersistenceService, secondaryTypeProperties); + + assertEquals(0, result.size()); + assertEquals(null, result.get("property1")); + assertEquals(null, result.get("property2")); + } + + @Test + public void testGetUpdatedSecondaryProperties_withPropertiesMapNull() { + // Mock the necessary components + CdsEntity mockEntity = mock(CdsEntity.class); + PersistenceService mockPersistenceService = mock(PersistenceService.class); + + // Prepare attachment and secondaryTypeProperties + Map attachment = new HashMap<>(); + attachment.put("ID", "123"); + + List secondaryTypeProperties = new ArrayList<>(); + + // Mock DBQuery class behavior + List propertiesInDB = new ArrayList<>(); + mockedDbQuery + .when( + () -> + DBQuery.getpropertiesForID( + mockEntity, mockPersistenceService, "123", secondaryTypeProperties)) + .thenReturn(propertiesInDB); + + Map result = + SDMUtils.getUpdatedSecondaryProperties( + Optional.of(mockEntity), attachment, mockPersistenceService, secondaryTypeProperties); + + assertEquals(0, result.size()); + assertEquals(null, result.get("property1")); + assertEquals(null, result.get("property2")); + } + + // @Test + // public void testGetUpdatedSecondaryProperties_DBPropertiesNull() { + // // Mock the necessary components + // CdsEntity mockEntity = mock(CdsEntity.class); + // PersistenceService mockPersistenceService = mock(PersistenceService.class); + + // // Prepare attachment and secondaryTypeProperties + // Map attachment = new HashMap<>(); + // attachment.put("ID", "123"); + // attachment.put("property1", "newValue1"); + // attachment.put("property2", "newValue2"); + + // List secondaryTypeProperties = Arrays.asList("property1", "property2"); + + // // Mock DBQuery class behavior + // List propertiesInDB = null; + // mockedDbQuery + // .when( + // () -> + // DBQuery.getpropertiesForID( + // mockEntity, mockPersistenceService, "123", secondaryTypeProperties)) + // .thenReturn(propertiesInDB); + + // Map result = + // SDMUtils.getUpdatedSecondaryProperties( + // Optional.of(mockEntity), attachment, mockPersistenceService, + // secondaryTypeProperties); + + // assertEquals(2, result.size()); + // assertEquals("newValue1", result.get("property1")); + // assertEquals("newValue2", result.get("property2")); + // } + + @Test + public void testGetUpdatedSecondaryProperties_withNoChanges() { + // Mock the necessary components + PersistenceService mockPersistenceService = mock(PersistenceService.class); + + // Prepare attachment and secondaryTypeProperties + Map attachment = new HashMap<>(); + attachment.put("ID", "123"); + attachment.put("property1", "sameValue1"); + attachment.put("property2", "sameValue2"); + + List secondaryTypeProperties = Arrays.asList("property1", "property2"); + + // Mock DBQuery static method behavior using try-with-resources + List propertiesInDB = Arrays.asList("sameValue1", "sameValue2"); + mockedDbQuery + .when( + () -> + DBQuery.getpropertiesForID( + mockEntity, mockPersistenceService, "123", secondaryTypeProperties)) + .thenReturn(propertiesInDB); + + // Call the method under test + Map result = + SDMUtils.getUpdatedSecondaryProperties( + Optional.of(mockEntity), attachment, mockPersistenceService, secondaryTypeProperties); + + // Validate results + assertTrue(result.isEmpty()); + } + + @Test + public void getSecondaryTypeProperties_whenAnnotationIsPresent() { + Optional attachmentEntity = Optional.of(mockEntity); + Map attachment = new HashMap<>(); + attachment.put("VALID_PROPERTY", new Object()); + when(mockEntity.getElement("VALID_PROPERTY")).thenReturn(mockElement); + when(mockElement.findAnnotation(SDMConstants.SDM_ANNOTATION_ADDITIONALPROPERTY)) + .thenReturn(Optional.of(mockAnnotation)); + when(mockElement.getName()).thenReturn("VALID_PROPERTY"); + + // Act: calling the method under test + List result = SDMUtils.getSecondaryTypeProperties(attachmentEntity, attachment); + + // Assert: we expect "VALID_PROPERTY" to be in the result + assertEquals(Collections.singletonList("VALID_PROPERTY"), result); + } + + @Test + public void testPropertyNullOrMissingMiscellaneous() throws IOException { + // Arrange + HttpEntity mockResponseEntity = mock(HttpEntity.class); + List secondaryPropertyIds = new ArrayList<>(); + + // Simulate response string with "propertyDefinitions" but no "mcm:miscellaneous" + String responseString = "{\"propertyDefinitions\": {\"key1\": {}}}"; + when(mockResponseEntity.getContent()) + .thenReturn(new java.io.ByteArrayInputStream(responseString.getBytes())); + + // Act + Boolean result = SDMUtils.checkMCM(mockResponseEntity, secondaryPropertyIds); + + // Assert + assertFalse(result); + assertTrue(secondaryPropertyIds.isEmpty()); // No property ID should be added + } + + @Test + public void testPropertyValueIsNullInMapAndNotNullInDB() { + // Arrange + Map attachment = new HashMap<>(); + attachment.put("ID", "12345"); // Sample ID + + // Simulating that "property1" has a null value in attachment map + attachment.put("property1", null); + + // Secondary type properties to check + List secondaryTypeProperties = Arrays.asList("property1", "property2"); + + // Simulate the database response where "property1" has a value in the DB + List propertiesInDB = Arrays.asList("DBValueForProperty1", "DBValueForProperty2"); + + // Mocking the DBQuery call to return propertiesInDB for "property1" + when(DBQuery.getpropertiesForID( + any(), eq(mockPersistenceService), eq("12345"), eq(secondaryTypeProperties))) + .thenReturn(propertiesInDB); + + Optional attachmentEntity = Optional.of(mock(CdsEntity.class)); + + // Act + Map result = + SDMUtils.getUpdatedSecondaryProperties( + attachmentEntity, attachment, mockPersistenceService, secondaryTypeProperties); + + // Assert + assertTrue(result.containsKey("property1")); + assertNull( + result.get( + "property1")); // Since property1 is null in attachment and non-null in DB, it should be + // set to null + } + + @Test + void testAttachmentEntityNotPresent() { + List result = + SDMUtils.getSecondaryTypeProperties(Optional.empty(), Map.of("key1", "value1")); + assertEquals(Collections.emptyList(), result); + } + + @Test + void testAttachmentEntityPresentNoMatchingKeys() { + CdsEntity entity = mock(CdsEntity.class); + when(entity.getElement(anyString())).thenReturn(null); + + List result = + SDMUtils.getSecondaryTypeProperties(Optional.of(entity), Map.of("key1", "value1")); + assertEquals(Collections.emptyList(), result); + } + + @Test + void testDraftReadonlyContextSkipped() { + CdsEntity entity = mock(CdsEntity.class); + List result = + SDMUtils.getSecondaryTypeProperties( + Optional.of(entity), Map.of("DRAFT_READONLY_CONTEXT", "value")); + assertEquals(Collections.emptyList(), result); + verify(entity, never()).getElement(anyString()); + } + + @Test + void testElementWithoutAnnotation() { + CdsEntity entity = mock(CdsEntity.class); + CdsElement element = mock(CdsElement.class); + when(entity.getElement("key1")).thenReturn(element); + when(element.findAnnotation(anyString())).thenReturn(Optional.empty()); + + List result = + SDMUtils.getSecondaryTypeProperties(Optional.of(entity), Map.of("key1", "value1")); + assertEquals(Collections.emptyList(), result); + } + + @Test + void testElementWithAnnotation() { + CdsEntity entity = mock(CdsEntity.class); + CdsElement element = mock(CdsElement.class); + @SuppressWarnings("unchecked") + CdsAnnotation annotation = mock(CdsAnnotation.class); + + when(entity.getElement("key1")).thenReturn(element); + when(element.findAnnotation(SDMConstants.SDM_ANNOTATION_ADDITIONALPROPERTY)) + .thenReturn(Optional.of(annotation)); + when(element.getName()).thenReturn("key1"); + + List result = + SDMUtils.getSecondaryTypeProperties(Optional.of(entity), Map.of("key1", "value1")); + assertEquals(List.of("key1"), result); + } }