diff --git a/README.md b/README.md index 3e28e996..0316d8cf 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,6 @@ This plugin can be consumed by the CAP application deployed on BTP to store thei - Attachment changelog: Provides the capability to view complete audit trail of attachments. - Localization of error messages and UI fields: Provides the capability to have the UI fields and error messages translated to the local language of the leading application. - Attachment Upload Status: Upload Status is the new field which displays the upload status of attachment when being uploaded. -- Attachment Virus Scanning: Provides the capability to scan uploaded attachments for viruses using either Malware Scan (synchronous) or Trend Micro Scan (asynchronous) during the upload process. ## Table of Contents @@ -43,7 +42,6 @@ This plugin can be consumed by the CAP application deployed on BTP to store thei - [Support for Link type attachments](#support-for-link-type-attachments) - [Support for Edit of Link type attachments](#support-for-edit-of-link-type-attachments) - [Support for Localization](#support-for-localization) -- [Support for Attachment Virus Scanning](#support-for-attachment-virus-scanning) - [Support for Attachment Upload Status](#support-for-attachment-upload-status) - [Known Restrictions](#known-restrictions) - [Support, Feedback, Contributing](#support-feedback-contributing) @@ -70,7 +68,7 @@ This plugin can be consumed by the CAP application deployed on BTP to store thei ## Setup -In this guide, we use the Bookshop sample app in the [deploy branch](https://github.com/cap-java/sdm/tree/deploy) of this repository, to integrate SDM CAP plugin. Follow the steps in this section for a quick way to deploy and test the plugin without needing to create your own custom CAP application. +In this guide, we use the Bookshop sample app in the [local_deploy branch](https://github.com/cap-java/sdm/tree/local_deploy) of this repository, to integrate SDM CAP plugin. Follow the steps in this section for a quick way to deploy and test the plugin without needing to create your own custom CAP application. ### Using the released version If you want to use the version of SDM CAP plugin released on the central maven repository follow the below steps: @@ -83,10 +81,10 @@ If you want to use the version of SDM CAP plugin released on the central maven r git clone https://github.com/cap-java/sdm ``` -3. Checkout to the branch **deploy**: +3. Checkout to the branch **local_deploy**: ```sh - git checkout deploy + git checkout local_deploy ``` 4. Navigate to the demoapp folder: @@ -130,10 +128,10 @@ To use a development version of the SDM CAP plugin, follow these steps. This is ``` The plugin is now added to your local .m2 repository, giving it priority over the version available in the central Maven repository during the application build. -3. Checkout to the branch **deploy**: +3. Checkout to the branch **local_deploy**: ```sh - git checkout deploy + git checkout local_deploy ``` 4. Navigate to the demoapp folder: @@ -1187,11 +1185,17 @@ annotate my.Books.attachments with @UI: { }, LineItem : [ {Value: type, @HTML5.CssDefaults: {width: '10%'}}, - {Value: fileName, @HTML5.CssDefaults: {width: '25%'}}, + {Value: fileName, @HTML5.CssDefaults: {width: '20%'}}, {Value: content, @HTML5.CssDefaults: {width: '0%'}}, {Value: createdAt, @HTML5.CssDefaults: {width: '20%'}}, {Value: createdBy, @HTML5.CssDefaults: {width: '20%'}}, - {Value: note, @HTML5.CssDefaults: {width: '25%'}}, + {Value: note, @HTML5.CssDefaults: {width: '20%'}}, + { + Value : uploadStatus, + Criticality: uploadStatusNav.criticality, + @Common.FieldControl: #ReadOnly, + @HTML5.CssDefaults: {width: '10%'} + }, { $Type : 'UI.DataFieldForActionGroup', ID : 'TableActionGroup', @@ -1276,48 +1280,6 @@ SDM.mimetypeInvalidError=Der Dateityp ist nicht zulässig SDM.maxCountErrorMessage=Maximale Anzahl von Anhängen erreicht ``` -## Support for Attachment Virus Scanning - -The SDM CAP plugin supports two types of virus scanning for uploaded attachments: - -### 1. Malware Scan - -Malware scanning can be enabled by setting the `VirusScanEnabled` property to `true` during repository onboarding. - -**Enable Malware Scan:** -```java -repository.setIsVirusScanEnabled(true); -``` - -**Workflow:** -- When a file is uploaded to a malware scan-enabled repository, the initial status displays as **"Uploading"**. -- If the attachment is clean, the status changes to **"Success"**. -- If a virus is detected, an error message is displayed and the file is automatically removed from the UI. - -**Limitations:** -- Malware scanning supports a maximum file size of **400 MB**. -- Files exceeding this limit cannot be scanned and will show an error. - -### 2. Trend Micro Scan - -Trend Micro scanning provides advanced virus detection capabilities with asynchronous processing. - -**Enable Trend Micro Scan:** -```java -repository.setIsAsyncVirusScanEnabled(true); -``` - -**Workflow:** -- When a file is uploaded to a Trend Micro scan-enabled repository, the initial status displays as **"Uploading"**. -- The status then transitions to **"Virus Scanning in Progress"**. -- After the scan completes: - - If the attachment is virus-free, the status changes to **"Success"** (requires page refresh). - - If a virus is detected, the status changes to **"Virus Detected"** and the user must manually delete the file before saving the entity. - -**Key Considerations:** -- There is no restriction on file size for Trend Micro scanning. -- Files with **"Virus Scanning in Progress"** or **"Virus Detected"** status cannot be downloaded or viewed. -- Users need to refresh the page to see updated scan results. ## Support for Attachment Upload Status @@ -1329,19 +1291,12 @@ The upload status transitions from "Uploading" to "Success". **For repositories with malware scanning:** The upload status transitions from "Uploading" to "Success" if no virus is detected. If a virus is detected, the attachment is automatically deleted. -**For repositories with Trend Micro virus scanning:** -The upload status transitions from "Uploading" to "Virus Scanning in Progress". After refreshing the page, if the scan completes successfully and the attachment is virus-free, the status changes to "Success". If a virus is detected, the status changes to "Virus Detected" and the user must manually delete the file before saving the entity. - -**Note:** Files with "Virus Scanning in Progress" or "Virus Detected" status cannot be downloaded or viewed. - To display color-coded status indicators in the UI, create a `sap.attachments-UploadScanStates.csv` file in the `db/data` folder with the following content: ``` code;name;criticality uploading;Uploading;5 Success;Success;3 Failed;Scan Failed;2 -VirusDetected;Virus detected;1 -VirusScanInprogress;Virus scanning inprogress(refresh page);5 ``` ## Known Restrictions diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandler.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandler.java index 9f04eea7..14914057 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandler.java @@ -122,13 +122,14 @@ public void processBefore(CdsReadEventContext context) throws IOException { sdmService.checkRepositoryType(repositoryId, context.getUserInfo().getTenant()); Optional attachmentDraftEntity = context.getModel().findEntity(context.getTarget().getQualifiedName() + "_drafts"); - + String upIdKey = "", upID = ""; if (attachmentDraftEntity.isPresent()) { - String upIdKey = SDMUtils.getUpIdKey(attachmentDraftEntity.get()); + upIdKey = SDMUtils.getUpIdKey(attachmentDraftEntity.get()); CqnSelect select = (CqnSelect) context.get("cqn"); - String upID = SDMUtils.fetchUPIDFromCQN(select, attachmentDraftEntity.get()); + upID = SDMUtils.fetchUPIDFromCQN(select, attachmentDraftEntity.get()); if (!repoValue.getIsAsyncVirusScanEnabled()) { + dbQuery.updateInProgressUploadStatusToSuccess( attachmentDraftEntity.get(), persistenceService, upID, upIdKey); } @@ -167,7 +168,6 @@ public Predicate where(Predicate where) { return CQL.and(where, repositoryFilter); } }); - setErrorMessagesInCache(context); context.setCqn(modifiedCqn); } catch (Exception e) { @@ -231,10 +231,16 @@ private void processVirusScanInProgressAttachments( // Get all attachments with virus scan in progress Optional attachmentDraftEntity = context.getModel().findEntity(context.getTarget().getQualifiedName() + "_drafts"); + Optional attachmentActiveEntity = + context.getModel().findEntity(context.getTarget().getQualifiedName()); List attachmentsInProgress = dbQuery.getAttachmentsWithVirusScanInProgress( - attachmentDraftEntity.get(), persistenceService, upID, upIDkey); + attachmentDraftEntity.orElse(null), + attachmentActiveEntity.orElse(null), + persistenceService, + upID, + upIDkey); // Get SDM credentials var sdmCredentials = tokenHandler.getSDMCredentials(); @@ -242,7 +248,11 @@ private void processVirusScanInProgressAttachments( // Iterate through each attachment and call getObject for (CmisDocument attachment : attachmentsInProgress) { processAttachmentVirusScanStatus( - attachment, sdmCredentials, attachmentDraftEntity.get(), persistenceService); + attachment, + sdmCredentials, + attachmentDraftEntity.orElse(null), + attachmentActiveEntity.orElse(null), + persistenceService); } if (!attachmentsInProgress.isEmpty()) { @@ -261,12 +271,14 @@ private void processVirusScanInProgressAttachments( * @param attachment the attachment document to process * @param sdmCredentials the SDM credentials for API calls * @param attachmentDraftEntity the draft entity for the attachment + * @param attachmentActiveEntity the active entity for the attachment * @param persistenceService the persistence service for database operations */ private void processAttachmentVirusScanStatus( CmisDocument attachment, SDMCredentials sdmCredentials, CdsEntity attachmentDraftEntity, + CdsEntity attachmentActiveEntity, PersistenceService persistenceService) { try { String objectId = attachment.getObjectId(); @@ -299,7 +311,11 @@ private void processAttachmentVirusScanStatus( if (scanStatus != null) { SDMConstants.ScanStatus scanStatusEnum = SDMConstants.ScanStatus.fromValue(scanStatus); dbQuery.updateUploadStatusByScanStatus( - attachmentDraftEntity, persistenceService, objectId, scanStatusEnum); + attachmentDraftEntity, + attachmentActiveEntity, + persistenceService, + objectId, + scanStatusEnum); logger.info( "Updated uploadStatus for objectId: {} based on scanStatus: {}", objectId, 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 4966f2fb..e5e3903e 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 @@ -505,29 +505,62 @@ public CmisDocument getuploadStatusForAttachment( } public List getAttachmentsWithVirusScanInProgress( - CdsEntity attachmentEntity, + CdsEntity attachmentDraftEntity, + CdsEntity attachmentActiveEntity, PersistenceService persistenceService, String upID, String upIDkey) { - CqnSelect q = - Select.from(attachmentEntity) - .columns( - "ID", - "objectId", - "fileName", - "folderId", - "repositoryId", - "mimeType", - "uploadStatus") - .where( - doc -> - doc.get(upIDkey) - .eq(upID) - .and(doc.get("uploadStatus").eq(SDMConstants.VIRUS_SCAN_INPROGRESS))); + List attachments = new ArrayList<>(); - Result result = persistenceService.run(q); + // Query draft table + if (attachmentDraftEntity != null) { + CqnSelect draftQuery = + Select.from(attachmentDraftEntity) + .columns( + "ID", + "objectId", + "fileName", + "folderId", + "repositoryId", + "mimeType", + "uploadStatus") + .where( + doc -> + doc.get(upIDkey) + .eq(upID) + .and(doc.get("uploadStatus").eq(SDMConstants.VIRUS_SCAN_INPROGRESS))); - List attachments = new ArrayList<>(); + Result draftResult = persistenceService.run(draftQuery); + attachments.addAll(mapResultToCmisDocuments(draftResult)); + } + + // Query active table + if (attachmentActiveEntity != null) { + CqnSelect activeQuery = + Select.from(attachmentActiveEntity) + .columns( + "ID", + "objectId", + "fileName", + "folderId", + "repositoryId", + "mimeType", + "uploadStatus") + .where( + doc -> + doc.get(upIDkey) + .eq(upID) + .and(doc.get("uploadStatus").eq(SDMConstants.VIRUS_SCAN_INPROGRESS))); + + Result activeResult = persistenceService.run(activeQuery); + attachments.addAll(mapResultToCmisDocuments(activeResult)); + } + + return attachments; + } + + private List mapResultToCmisDocuments(Result result) { + List documents = new ArrayList<>(); for (Row row : result.list()) { CmisDocument cmisDocument = new CmisDocument(); cmisDocument.setAttachmentId(row.get("ID") != null ? row.get("ID").toString() : null); @@ -541,9 +574,72 @@ public List getAttachmentsWithVirusScanInProgress( row.get("uploadStatus") != null ? row.get("uploadStatus").toString() : SDMConstants.UPLOAD_STATUS_IN_PROGRESS); - attachments.add(cmisDocument); + documents.add(cmisDocument); + } + return documents; + } + + /** + * Deletes draft entries from the attachment entity where objectId is null and uploadStatus is + * 'uploading'. This is used to clean up incomplete upload entries when the application is + * refreshed. + * + * @param attachmentEntity the draft attachment entity to delete from + * @param persistenceService the persistence service to use for database operations + * @param upID the up__ID to filter attachments + * @param upIdKey the key name for up__ID field (e.g., "up__ID") + */ + public void deleteAttachmentsWithNullObjectIdAndUploadingStatus( + CdsEntity attachmentEntity, + PersistenceService persistenceService, + String upID, + String upIdKey) { + var deleteQuery = + Delete.from(attachmentEntity) + .where( + doc -> + doc.get(upIdKey) + .eq(upID) + .and(doc.get("objectId").isNull()) + .and(doc.get("uploadStatus").eq(SDMConstants.UPLOAD_STATUS_IN_PROGRESS))); + Result result = persistenceService.run(deleteQuery); + if (result.rowCount() > 0) { + logger.info( + "Deleted {} attachment(s) with null objectId and uploading status for upID: {}", + result.rowCount(), + upID); + } + } + + /** + * Deletes draft entries from the attachment entity where both objectId and folderId are null. + * This is used to clean up failed or incomplete upload entries. + * + * @param attachmentEntity the draft attachment entity to delete from + * @param persistenceService the persistence service to use for database operations + * @param upID the up__ID to filter attachments + * @param upIdKey the key name for up__ID field (e.g., "up__ID") + */ + public void deleteDraftEntriesWithNullObjectIdAndFolderId( + CdsEntity attachmentEntity, + PersistenceService persistenceService, + String upID, + String upIdKey) { + var deleteQuery = + Delete.from(attachmentEntity) + .where( + doc -> + doc.get(upIdKey) + .eq(upID) + .and(doc.get("objectId").isNull()) + .and(doc.get("folderId").isNull())); + Result result = persistenceService.run(deleteQuery); + if (result.rowCount() > 0) { + logger.info( + "Deleted {} draft entries with null objectId and folderId for upID: {}", + result.rowCount(), + upID); } - return attachments; } /** @@ -591,17 +687,48 @@ public void updateInProgressUploadStatusToSuccess( } public Result updateUploadStatusByScanStatus( - CdsEntity attachmentEntity, + CdsEntity attachmentDraftEntity, + CdsEntity attachmentActiveEntity, PersistenceService persistenceService, String objectId, SDMConstants.ScanStatus scanStatus) { String uploadStatus = mapScanStatusToUploadStatus(scanStatus); - CqnUpdate updateQuery = - Update.entity(attachmentEntity) - .data("uploadStatus", uploadStatus) - .where(doc -> doc.get("objectId").eq(objectId)); + Result combinedResult = null; + long totalRowCount = 0L; + + // Update draft table + if (attachmentDraftEntity != null) { + CqnUpdate draftUpdateQuery = + Update.entity(attachmentDraftEntity) + .data("uploadStatus", uploadStatus) + .where(doc -> doc.get("objectId").eq(objectId)); + Result draftResult = persistenceService.run(draftUpdateQuery); + totalRowCount += draftResult.rowCount(); + combinedResult = draftResult; + } + + // Update active table + if (attachmentActiveEntity != null) { + CqnUpdate activeUpdateQuery = + Update.entity(attachmentActiveEntity) + .data("uploadStatus", uploadStatus) + .where(doc -> doc.get("objectId").eq(objectId)); + Result activeResult = persistenceService.run(activeUpdateQuery); + totalRowCount += activeResult.rowCount(); + if (combinedResult == null) { + combinedResult = activeResult; + } + } + + if (totalRowCount > 0) { + logger.info( + "Updated {} record(s) with objectId: {} to uploadStatus: {}", + totalRowCount, + objectId, + uploadStatus); + } - return persistenceService.run(updateQuery); + return combinedResult; } private String mapScanStatusToUploadStatus(SDMConstants.ScanStatus scanStatus) { diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandlerTest.java index ad7f9b27..cf16d73d 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandlerTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandlerTest.java @@ -65,7 +65,6 @@ void testModifyCqnForAttachmentsEntity_Success() throws IOException { try (MockedStatic sdmUtilsMock = Mockito.mockStatic(SDMUtils.class)) { sdmUtilsMock.when(() -> SDMUtils.getUpIdKey(any())).thenReturn("mockUpIdKey"); sdmUtilsMock.when(() -> SDMUtils.fetchUPIDFromCQN(any(), any())).thenReturn("mockUpID"); - doNothing() .when(dbQuery) .updateInProgressUploadStatusToSuccess(any(), any(), anyString(), anyString()); @@ -202,4 +201,94 @@ void testProcessBefore_WithCollectionReadNoKeys() throws IOException { // Assert - repositoryId filter should be added for collection reads verify(context).setCqn(any(CqnSelect.class)); } + + @Test + void testProcessBefore_DeleteDraftEntriesWithNullObjectIdAndFolderId() throws IOException { + // Arrange + CqnSelect select = + Select.from(cdsEntity).where(doc -> doc.get("repositoryId").eq(REPOSITORY_ID_KEY)); + when(context.getTarget()).thenReturn(cdsEntity); + when(cdsEntity.getAnnotationValue(SDMConstants.ANNOTATION_IS_MEDIA_DATA, false)) + .thenReturn(true); + when(context.getCqn()).thenReturn(select); + RepoValue repoValue = new RepoValue(); + repoValue.setIsAsyncVirusScanEnabled(false); + when(sdmService.checkRepositoryType(any(), any())).thenReturn(repoValue); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("tenant1"); + + CdsEntity attachmentDraftEntity = Mockito.mock(CdsEntity.class); + CdsModel model = Mockito.mock(CdsModel.class); + when(context.getModel()).thenReturn(model); + when(model.findEntity(anyString())).thenReturn(Optional.of(attachmentDraftEntity)); + when(cdsEntity.getQualifiedName()).thenReturn("TestEntity"); + when(context.get("cqn")).thenReturn(select); + + try (MockedStatic sdmUtilsMock = Mockito.mockStatic(SDMUtils.class)) { + sdmUtilsMock.when(() -> SDMUtils.getUpIdKey(any())).thenReturn("mockUpIdKey"); + sdmUtilsMock.when(() -> SDMUtils.fetchUPIDFromCQN(any(), any())).thenReturn("mockUpID"); + + doNothing() + .when(dbQuery) + .updateInProgressUploadStatusToSuccess(any(), any(), anyString(), anyString()); + + // Act + sdmReadAttachmentsHandler.processBefore(context); + + // Assert - verify deleteDraftEntriesWithNullObjectIdAndFolderId is called before + // updateInProgressUploadStatusToSuccess + verify(dbQuery) + .updateInProgressUploadStatusToSuccess(any(), any(), eq("mockUpID"), eq("mockUpIdKey")); + verify(context).setCqn(any(CqnSelect.class)); + } + } + + @Test + void testProcessBefore_SingleEntityRead_NoDelete() throws IOException { + // Arrange - simulate a single entity read with ID in where clause + CqnSelect select = + Select.from(cdsEntity) + .where( + doc -> + doc.get("ID") + .eq("test-id-123") + .and(doc.get("repositoryId").eq(REPOSITORY_ID_KEY))); + when(context.getTarget()).thenReturn(cdsEntity); + when(cdsEntity.getAnnotationValue(SDMConstants.ANNOTATION_IS_MEDIA_DATA, false)) + .thenReturn(true); + when(context.getCqn()).thenReturn(select); + when(context.get("cqn")).thenReturn(select); + + RepoValue repoValue = new RepoValue(); + repoValue.setIsAsyncVirusScanEnabled(false); + when(sdmService.checkRepositoryType(any(), any())).thenReturn(repoValue); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("tenant1"); + + CdsEntity attachmentDraftEntity = Mockito.mock(CdsEntity.class); + CdsModel model = Mockito.mock(CdsModel.class); + when(context.getModel()).thenReturn(model); + when(model.findEntity(anyString())).thenReturn(Optional.of(attachmentDraftEntity)); + when(cdsEntity.getQualifiedName()).thenReturn("TestEntity"); + + try (MockedStatic sdmUtilsMock = Mockito.mockStatic(SDMUtils.class)) { + sdmUtilsMock.when(() -> SDMUtils.getUpIdKey(any())).thenReturn("mockUpIdKey"); + sdmUtilsMock.when(() -> SDMUtils.fetchUPIDFromCQN(any(), any())).thenReturn("mockUpID"); + doNothing() + .when(dbQuery) + .updateInProgressUploadStatusToSuccess(any(), any(), anyString(), anyString()); + + // Act + sdmReadAttachmentsHandler.processBefore(context); + + // Assert - deleteAttachmentsWithNullObjectIdAndUploadingStatus should NOT be called for + // single entity reads (where clause contains ID =) + verify(dbQuery, never()) + .deleteAttachmentsWithNullObjectIdAndUploadingStatus( + any(), any(), anyString(), anyString()); + verify(dbQuery) + .updateInProgressUploadStatusToSuccess(any(), any(), eq("mockUpID"), eq("mockUpIdKey")); + verify(context).setCqn(any(CqnSelect.class)); + } + } } diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/persistence/DBQueryTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/persistence/DBQueryTest.java new file mode 100644 index 00000000..e4228af8 --- /dev/null +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/persistence/DBQueryTest.java @@ -0,0 +1,344 @@ +package unit.com.sap.cds.sdm.persistence; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import com.sap.cds.Result; +import com.sap.cds.Row; +import com.sap.cds.ql.cqn.CqnSelect; +import com.sap.cds.ql.cqn.CqnUpdate; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.model.CmisDocument; +import com.sap.cds.sdm.persistence.DBQuery; +import com.sap.cds.services.persistence.PersistenceService; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DBQueryTest { + + @Mock private CdsEntity mockDraftEntity; + @Mock private CdsEntity mockActiveEntity; + @Mock private PersistenceService mockPersistenceService; + @Mock private Result mockResult; + @Mock private Row mockRow; + + private DBQuery dbQuery; + + @BeforeEach + void setUp() { + dbQuery = DBQuery.getDBQueryInstance(); + } + + @Test + void testGetAttachmentsWithVirusScanInProgress_BothTables() { + // Arrange + String upID = "testUpID"; + String upIDkey = "up__ID"; + + // Mock draft table result + Row draftRow = mock(Row.class); + when(draftRow.get("ID")).thenReturn("draft-id-1"); + when(draftRow.get("objectId")).thenReturn("object-1"); + when(draftRow.get("fileName")).thenReturn("draft-file.pdf"); + when(draftRow.get("folderId")).thenReturn("folder-1"); + when(draftRow.get("repositoryId")).thenReturn("repo-1"); + when(draftRow.get("mimeType")).thenReturn("application/pdf"); + when(draftRow.get("uploadStatus")).thenReturn(SDMConstants.VIRUS_SCAN_INPROGRESS); + + Result draftResult = mock(Result.class); + when(draftResult.list()).thenReturn(List.of(draftRow)); + + // Mock active table result + Row activeRow = mock(Row.class); + when(activeRow.get("ID")).thenReturn("active-id-1"); + when(activeRow.get("objectId")).thenReturn("object-2"); + when(activeRow.get("fileName")).thenReturn("active-file.pdf"); + when(activeRow.get("folderId")).thenReturn("folder-2"); + when(activeRow.get("repositoryId")).thenReturn("repo-1"); + when(activeRow.get("mimeType")).thenReturn("application/pdf"); + when(activeRow.get("uploadStatus")).thenReturn(SDMConstants.VIRUS_SCAN_INPROGRESS); + + Result activeResult = mock(Result.class); + when(activeResult.list()).thenReturn(List.of(activeRow)); + + when(mockPersistenceService.run(any(CqnSelect.class))) + .thenReturn(draftResult) + .thenReturn(activeResult); + + // Act + List result = + dbQuery.getAttachmentsWithVirusScanInProgress( + mockDraftEntity, mockActiveEntity, mockPersistenceService, upID, upIDkey); + + // Assert + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals("draft-id-1", result.get(0).getAttachmentId()); + assertEquals("object-1", result.get(0).getObjectId()); + assertEquals("draft-file.pdf", result.get(0).getFileName()); + assertEquals("active-id-1", result.get(1).getAttachmentId()); + assertEquals("object-2", result.get(1).getObjectId()); + assertEquals("active-file.pdf", result.get(1).getFileName()); + verify(mockPersistenceService, times(2)).run(any(CqnSelect.class)); + } + + @Test + void testGetAttachmentsWithVirusScanInProgress_DraftTableOnly() { + // Arrange + String upID = "testUpID"; + String upIDkey = "up__ID"; + + Row draftRow = mock(Row.class); + when(draftRow.get("ID")).thenReturn("draft-id-1"); + when(draftRow.get("objectId")).thenReturn("object-1"); + when(draftRow.get("fileName")).thenReturn("draft-file.pdf"); + when(draftRow.get("folderId")).thenReturn("folder-1"); + when(draftRow.get("repositoryId")).thenReturn("repo-1"); + when(draftRow.get("mimeType")).thenReturn("application/pdf"); + when(draftRow.get("uploadStatus")).thenReturn(SDMConstants.VIRUS_SCAN_INPROGRESS); + + Result draftResult = mock(Result.class); + when(draftResult.list()).thenReturn(List.of(draftRow)); + when(mockPersistenceService.run(any(CqnSelect.class))).thenReturn(draftResult); + + // Act + List result = + dbQuery.getAttachmentsWithVirusScanInProgress( + mockDraftEntity, null, mockPersistenceService, upID, upIDkey); + + // Assert + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("draft-id-1", result.get(0).getAttachmentId()); + verify(mockPersistenceService, times(1)).run(any(CqnSelect.class)); + } + + @Test + void testGetAttachmentsWithVirusScanInProgress_ActiveTableOnly() { + // Arrange + String upID = "testUpID"; + String upIDkey = "up__ID"; + + Row activeRow = mock(Row.class); + when(activeRow.get("ID")).thenReturn("active-id-1"); + when(activeRow.get("objectId")).thenReturn("object-1"); + when(activeRow.get("fileName")).thenReturn("active-file.pdf"); + when(activeRow.get("folderId")).thenReturn("folder-1"); + when(activeRow.get("repositoryId")).thenReturn("repo-1"); + when(activeRow.get("mimeType")).thenReturn("application/pdf"); + when(activeRow.get("uploadStatus")).thenReturn(SDMConstants.VIRUS_SCAN_INPROGRESS); + + Result activeResult = mock(Result.class); + when(activeResult.list()).thenReturn(List.of(activeRow)); + when(mockPersistenceService.run(any(CqnSelect.class))).thenReturn(activeResult); + + // Act + List result = + dbQuery.getAttachmentsWithVirusScanInProgress( + null, mockActiveEntity, mockPersistenceService, upID, upIDkey); + + // Assert + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("active-id-1", result.get(0).getAttachmentId()); + verify(mockPersistenceService, times(1)).run(any(CqnSelect.class)); + } + + @Test + void testGetAttachmentsWithVirusScanInProgress_NullEntities() { + // Arrange + String upID = "testUpID"; + String upIDkey = "up__ID"; + + // Act + List result = + dbQuery.getAttachmentsWithVirusScanInProgress( + null, null, mockPersistenceService, upID, upIDkey); + + // Assert + assertNotNull(result); + assertEquals(0, result.size()); + verify(mockPersistenceService, never()).run(any(CqnSelect.class)); + } + + @Test + void testGetAttachmentsWithVirusScanInProgress_WithNullFields() { + // Arrange + String upID = "testUpID"; + String upIDkey = "up__ID"; + + Row draftRow = mock(Row.class); + when(draftRow.get("ID")).thenReturn(null); + when(draftRow.get("objectId")).thenReturn(null); + when(draftRow.get("fileName")).thenReturn(null); + when(draftRow.get("folderId")).thenReturn(null); + when(draftRow.get("repositoryId")).thenReturn(null); + when(draftRow.get("mimeType")).thenReturn(null); + when(draftRow.get("uploadStatus")).thenReturn(null); + + Result draftResult = mock(Result.class); + when(draftResult.list()).thenReturn(List.of(draftRow)); + when(mockPersistenceService.run(any(CqnSelect.class))).thenReturn(draftResult); + + // Act + List result = + dbQuery.getAttachmentsWithVirusScanInProgress( + mockDraftEntity, null, mockPersistenceService, upID, upIDkey); + + // Assert + assertNotNull(result); + assertEquals(1, result.size()); + assertNull(result.get(0).getAttachmentId()); + assertNull(result.get(0).getObjectId()); + assertNull(result.get(0).getFileName()); + assertEquals(SDMConstants.UPLOAD_STATUS_IN_PROGRESS, result.get(0).getUploadStatus()); + } + + @Test + void testUpdateUploadStatusByScanStatus_BothTables() { + // Arrange + String objectId = "object-123"; + SDMConstants.ScanStatus scanStatus = SDMConstants.ScanStatus.CLEAN; + + Result draftResult = mock(Result.class); + Result activeResult = mock(Result.class); + when(draftResult.rowCount()).thenReturn(1L); + when(activeResult.rowCount()).thenReturn(1L); + + when(mockPersistenceService.run(any(CqnUpdate.class))) + .thenReturn(draftResult) + .thenReturn(activeResult); + + // Act + Result result = + dbQuery.updateUploadStatusByScanStatus( + mockDraftEntity, mockActiveEntity, mockPersistenceService, objectId, scanStatus); + + // Assert + assertNotNull(result); + verify(mockPersistenceService, times(2)).run(any(CqnUpdate.class)); + } + + @Test + void testUpdateUploadStatusByScanStatus_DraftTableOnly() { + // Arrange + String objectId = "object-123"; + SDMConstants.ScanStatus scanStatus = SDMConstants.ScanStatus.QUARANTINED; + + Result draftResult = mock(Result.class); + when(draftResult.rowCount()).thenReturn(1L); + when(mockPersistenceService.run(any(CqnUpdate.class))).thenReturn(draftResult); + + // Act + Result result = + dbQuery.updateUploadStatusByScanStatus( + mockDraftEntity, null, mockPersistenceService, objectId, scanStatus); + + // Assert + assertNotNull(result); + verify(mockPersistenceService, times(1)).run(any(CqnUpdate.class)); + } + + @Test + void testUpdateUploadStatusByScanStatus_ActiveTableOnly() { + // Arrange + String objectId = "object-123"; + SDMConstants.ScanStatus scanStatus = SDMConstants.ScanStatus.SCANNING; + + Result activeResult = mock(Result.class); + when(activeResult.rowCount()).thenReturn(1L); + when(mockPersistenceService.run(any(CqnUpdate.class))).thenReturn(activeResult); + + // Act + Result result = + dbQuery.updateUploadStatusByScanStatus( + null, mockActiveEntity, mockPersistenceService, objectId, scanStatus); + + // Assert + assertNotNull(result); + verify(mockPersistenceService, times(1)).run(any(CqnUpdate.class)); + } + + @Test + void testUpdateUploadStatusByScanStatus_NullEntities() { + // Arrange + String objectId = "object-123"; + SDMConstants.ScanStatus scanStatus = SDMConstants.ScanStatus.CLEAN; + + // Act + Result result = + dbQuery.updateUploadStatusByScanStatus( + null, null, mockPersistenceService, objectId, scanStatus); + + // Assert + assertNull(result); + verify(mockPersistenceService, never()).run(any(CqnUpdate.class)); + } + + @Test + void testUpdateUploadStatusByScanStatus_NoRecordsUpdated() { + // Arrange + String objectId = "object-123"; + SDMConstants.ScanStatus scanStatus = SDMConstants.ScanStatus.CLEAN; + + Result draftResult = mock(Result.class); + when(draftResult.rowCount()).thenReturn(0L); + when(mockPersistenceService.run(any(CqnUpdate.class))).thenReturn(draftResult); + + // Act + Result result = + dbQuery.updateUploadStatusByScanStatus( + mockDraftEntity, null, mockPersistenceService, objectId, scanStatus); + + // Assert + assertNotNull(result); + verify(mockPersistenceService, times(1)).run(any(CqnUpdate.class)); + } + + @Test + void testUpdateUploadStatusByScanStatus_AllScanStatuses() { + // Test all scan status mappings + String objectId = "object-123"; + Result mockResult = mock(Result.class); + when(mockResult.rowCount()).thenReturn(1L); + when(mockPersistenceService.run(any(CqnUpdate.class))).thenReturn(mockResult); + + // Test QUARANTINED -> UPLOAD_STATUS_VIRUS_DETECTED + dbQuery.updateUploadStatusByScanStatus( + mockDraftEntity, + null, + mockPersistenceService, + objectId, + SDMConstants.ScanStatus.QUARANTINED); + + // Test PENDING -> UPLOAD_STATUS_IN_PROGRESS + dbQuery.updateUploadStatusByScanStatus( + mockDraftEntity, null, mockPersistenceService, objectId, SDMConstants.ScanStatus.PENDING); + + // Test SCANNING -> VIRUS_SCAN_INPROGRESS + dbQuery.updateUploadStatusByScanStatus( + mockDraftEntity, null, mockPersistenceService, objectId, SDMConstants.ScanStatus.SCANNING); + + // Test FAILED -> UPLOAD_STATUS_SCAN_FAILED + dbQuery.updateUploadStatusByScanStatus( + mockDraftEntity, null, mockPersistenceService, objectId, SDMConstants.ScanStatus.FAILED); + + // Test CLEAN -> UPLOAD_STATUS_SUCCESS + dbQuery.updateUploadStatusByScanStatus( + mockDraftEntity, null, mockPersistenceService, objectId, SDMConstants.ScanStatus.CLEAN); + + // Test BLANK -> UPLOAD_STATUS_SUCCESS + dbQuery.updateUploadStatusByScanStatus( + mockDraftEntity, null, mockPersistenceService, objectId, SDMConstants.ScanStatus.BLANK); + + // Verify all updates were called + verify(mockPersistenceService, times(6)).run(any(CqnUpdate.class)); + } +}