diff --git a/README.md b/README.md index 3f1d26b8b..ddd818ac5 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ This plugin can be consumed by the CAP application deployed on BTP to store thei - Move attachments: Provides the capability to move attachments from one entity to another entity. - 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. + ## Table of Contents - [Pre-Requisites](#pre-requisites) @@ -40,6 +42,7 @@ 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 Upload Status](#support-for-attachment-upload-status) - [Known Restrictions](#known-restrictions) - [Support, Feedback, Contributing](#support-feedback-contributing) - [Code of Conduct](#code-of-conduct) @@ -53,10 +56,10 @@ This plugin can be consumed by the CAP application deployed on BTP to store thei > **cds-services** > -> The behaviour of clicking attachment and previewing it varies based on the version of cds-services used by the CAP application. +> The behaviour of clicking attachment and previewing it varies based on the version of cds-services used by the CAP application. > > - For cds-services version >= 3.4.0, clicking on attachment will -> - open the file in new browser tab, if browser supports the file type. + > - open the file in new browser tab, if browser supports the file type. > - download the file to the computer, if browser does not support the file type. > > - For cds-services version < 3.4.0, clicking on attachment will download the file to the computer @@ -90,7 +93,7 @@ If you want to use the version of SDM CAP plugin released on the central maven r cd cap-notebook/demoapp ``` -5. Configure the [REPOSITORY_ID](https://github.com/cap-java/sdm/blob/4180e501ecd792770174aa4972b06aff54ac139d/cap-notebook/demoapp/mta.yaml#L21) with the repository you want to use for deploying the application. Set the SDM instance name to match the SAP Document Management integration option instance you created in BTP and update this in the mta.yaml file under the [srv module](https://github.com/cap-java/sdm/blob/4180e501ecd792770174aa4972b06aff54ac139d/cap-notebook/demoapp/mta.yaml#L31) and the [resources section](https://github.com/cap-java/sdm/blob/4180e501ecd792770174aa4972b06aff54ac139d/cap-notebook/demoapp/mta.yaml#L98) values in the **mta.yaml**. +5. Configure the [REPOSITORY_ID](https://github.com/cap-java/sdm/blob/4180e501ecd792770174aa4972b06aff54ac139d/cap-notebook/demoapp/mta.yaml#L21) with the repository you want to use for deploying the application. Set the SDM instance name to match the SAP Document Management integration option instance you created in BTP and update this in the mta.yaml file under the [srv module](https://github.com/cap-java/sdm/blob/4180e501ecd792770174aa4972b06aff54ac139d/cap-notebook/demoapp/mta.yaml#L31) and the [resources section](https://github.com/cap-java/sdm/blob/4180e501ecd792770174aa4972b06aff54ac139d/cap-notebook/demoapp/mta.yaml#L98) values in the **mta.yaml**. 6. Build the application: @@ -137,7 +140,7 @@ The plugin is now added to your local .m2 repository, giving it priority over th cd cap-notebook/demoapp ``` -5. Configure the [REPOSITORY_ID](https://github.com/cap-java/sdm/blob/4180e501ecd792770174aa4972b06aff54ac139d/cap-notebook/demoapp/mta.yaml#L21) with the repository you want to use for deploying the application. Set the SDM instance name to match the SAP Document Management integration option instance you created in BTP and update this in the mta.yaml file under the [srv module](https://github.com/cap-java/sdm/blob/4180e501ecd792770174aa4972b06aff54ac139d/cap-notebook/demoapp/mta.yaml#L31) and the [resources section](https://github.com/cap-java/sdm/blob/4180e501ecd792770174aa4972b06aff54ac139d/cap-notebook/demoapp/mta.yaml#L98) values in the **mta.yaml**. +5. Configure the [REPOSITORY_ID](https://github.com/cap-java/sdm/blob/4180e501ecd792770174aa4972b06aff54ac139d/cap-notebook/demoapp/mta.yaml#L21) with the repository you want to use for deploying the application. Set the SDM instance name to match the SAP Document Management integration option instance you created in BTP and update this in the mta.yaml file under the [srv module](https://github.com/cap-java/sdm/blob/4180e501ecd792770174aa4972b06aff54ac139d/cap-notebook/demoapp/mta.yaml#L31) and the [resources section](https://github.com/cap-java/sdm/blob/4180e501ecd792770174aa4972b06aff54ac139d/cap-notebook/demoapp/mta.yaml#L98) values in the **mta.yaml**. 6. Build the application: @@ -156,10 +159,10 @@ The plugin is now added to your local .m2 repository, giving it priority over th ``` ## Use com.sap.cds:sdm dependency -Follow these steps if you want to integrate the SDM CAP Plugin with your own CAP application. +Follow these steps if you want to integrate the SDM CAP Plugin with your own CAP application. 1. Add the following dependency in pom.xml in the srv folder - + ```xml com.sap.cds @@ -201,7 +204,7 @@ Follow these steps if you want to integrate the SDM CAP Plugin with your own CAP ```` After that the models can be used. - + 2. To use sdm plugin in your CAP application, create an element with an `Attachments` type. Following the [best practice of separation of concerns](https://cap.cloud.sap/docs/guides/domain-modeling#separation-of-concerns), create a separate file _srv/attachment-extension.cds_ and extend your entity with attachments. Refer the following example from a sample Bookshop app: ```cds @@ -231,7 +234,7 @@ Follow these steps if you want to integrate the SDM CAP Plugin with your own CAP service-plan: standard ``` -4. Using the created SDM instance's credentials from key [onboard a repository](https://help.sap.com/docs/document-management-service/sap-document-management-service/onboarding-repository). In mta.yaml, under properties of the srv module add the repository id. Refer the following example from a sample Bookshop app. Currently only non versioned repositories are supported. +4. Using the created SDM instance's credentials from key [onboard a repository](https://help.sap.com/docs/document-management-service/sap-document-management-service/onboarding-repository). In mta.yaml, under properties of the srv module add the repository id. Refer the following example from a sample Bookshop app. Currently only non versioned repositories are supported. ```yaml modules: @@ -317,7 +320,7 @@ Follow these steps if you want to integrate the SDM CAP Plugin with your own CAP Delete an attachment -8. **Rename a file** by going into Edit mode and setting a new name for the file in the filename field. Then click the **Save** button to have that file renamed in SAP Document Management Integration Option. We demonstrate this by renaming the previously uploaded TXT file: +8. **Rename a file** by going into Edit mode and setting a new name for the file in the filename field. Then click the **Save** button to have that file renamed in SAP Document Management Integration Option. We demonstrate this by renaming the previously uploaded TXT file: Delete an attachment @@ -327,7 +330,7 @@ Follow these steps if you want to integrate the SDM CAP Plugin with your own CAP ## Support for Multitenancy -This plugin provides APIs for onboarding and offboarding of repositories for multitenant CAP SaaS applications. +This plugin provides APIs for onboarding and offboarding of repositories for multitenant CAP SaaS applications. GetDependencies, subscribe and unsubscribe are the mandatory steps to be performed to support multitenancy. @@ -356,7 +359,7 @@ return sdmBinding.getCredentials(); } ``` Refer the below example where onboarding and offboarding APIs are used on tenant subscription and tenant unsubscription events of SaaS application. - + ```java @After(event = DeploymentService.EVENT_SUBSCRIBE) public void onSubscribe(SubscribeEventContext context) { @@ -435,10 +438,10 @@ Custom properties are supported via the usage of CMIS secondary type properties. ``` 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.name`. - - In this field set the name of the secondary property in SDM. - + - Extend the `Attachments` aspect with the secondary properties in the previously created _attachment-extension.cds_ file. + - Annotate the secondary properties with `@SDM.Attachments.AdditionalProperty.name`. + - In this field set the name of the secondary property in SDM. + Refer the following example from a sample Bookshop app: ```cds @@ -453,7 +456,7 @@ Custom properties are supported via the usage of CMIS secondary type properties. > **Note** > - > SDM supports secondary properties with data types `String`, `Boolean`, `Decimal`, `Integer` and `DateTime`. + > SDM supports secondary properties with data types `String`, `Boolean`, `Decimal`, `Integer` and `DateTime`. ## Support for Maximum allowed uploads This plugin allows you to customize the maximum number of uploads a user can perform. Once a user exceeds the defined limit, any further upload attempts will trigger an error. The error message shown to the user is also fully customizable. The annotation `@SDM.Attachments` should be used for defining the maximum upload limit. @@ -481,9 +484,9 @@ Example for German language in `messages_de.properties`: SDM.maxCountErrorMessage = Maximale Anzahl von Anhängen erreicht ``` - > **Note** - > - > Once the maxCount is configured, it is recommended not to alter it. If the maxCount is altered, the previously uploaded documents will still be visible. +> **Note** +> +> Once the maxCount is configured, it is recommended not to alter it. If the maxCount is altered, the previously uploaded documents will still be visible. ## Support for Multiple attachment facets The plugin supports creating multiple attachment facets or sections, each allowing various documents to be uploaded. The names of these facets are fully customizable. All existing operations available for the default attachment facet are also supported for any additional facets you create. @@ -522,9 +525,9 @@ Add the following facet in _fiori-service.cds_ in the _app_ folder. Refer the fo } ``` - > **Note** - > - > Once a facet or section name is defined in the CDS file, it is strongly recommended not to modify it. For instance, in the example provided, section names such as attachments, references, and footnotes should remain unchanged after initial configuration. Renaming these sections will result in the creation of new tables, causing any data associated with the original sections to become inaccessible in the UI. +> **Note** +> +> Once a facet or section name is defined in the CDS file, it is strongly recommended not to modify it. For instance, in the example provided, section names such as attachments, references, and footnotes should remain unchanged after initial configuration. Renaming these sections will result in the creation of new tables, causing any data associated with the original sections to become inaccessible in the UI. ## Support for technical user The CAP OData operations can be performed on attachments using a technical user. This flow can be used for machine-to-machine (M2M) interactions, where user involvement is not necessary. @@ -549,7 +552,7 @@ request = This plugin provides capability to copy attachments from one entity to another. This capability will copy attachments metadata on CAP as well as actual content on the SAP Document Management service repository. This feature can be used in following two ways. 1. **A helper method to copy attachments from one entity to another** - + The `AttachmentService` instance can be used to call `copyAttachments` method. This method expects an object of `CopyAttachmentInput` which requires new entity's Id (`up__Id`), the `attachments facet name` and the `list of objectIds` corresponding to attachments that are to be copied. Example usage: @@ -563,7 +566,7 @@ This plugin provides capability to copy attachments from one entity to another. attachmentService.copyAttachments(copyEventInput, isSystemUser); ``` 2. **OData API to copy attachments from one entity to another** - + You can also use an OData API call to trigger the copy operation. `AttachmentsService` endpoint URL can be used with suffix `/.copyAttachments` . This request expects the following request body: ```json @@ -573,7 +576,7 @@ This plugin provides capability to copy attachments from one entity to another. } ``` - Example usage: + Example usage: ``` HTTP Method: POST Request URL: @@ -600,7 +603,7 @@ This plugin provides capability to move attachments from one entity to another e ### Usage Methods 1. **A helper method to move attachments from one entity to another** - + The `AttachmentService` instance can be used to call `moveAttachments` method. This method expects an object of `MoveAttachmentInput` which requires the source folder ID, target entity's ID (`up__Id`), the `attachments facet name` and the `list of objectIds` corresponding to attachments that are to be moved. Example usage: @@ -632,7 +635,7 @@ This plugin provides capability to move attachments from one entity to another e ``` 2. **OData API to move attachments from one entity to another** - + You can also use an OData API call to trigger the move operation. `AttachmentsService` endpoint URL can be used with suffix `/.moveAttachments`. This request expects the following request body: ```json @@ -657,7 +660,7 @@ This plugin provides capability to move attachments from one entity to another e "objectIds": ["abc", "xyz"] } ``` - + Note: The `facet` parameter should be the fully qualified name of the target attachment composition (e.g., `AdminService.Books.attachments`). ### Optional Parameters @@ -740,12 +743,12 @@ The changelog functionality retrieves the complete history of an attachment from To enable changelog viewing in your CAP application: -1. **Add a custom controller extension** +1. **Add a custom controller extension** In webapp/controller/custom.controller.js, copy and paste below content. - + See this [example](https://github.com/cap-java/sdm/blob/develop_deploy/cap-notebook/demoapp/app/admin-books/webapp/controller/custom.controller.js) from a sample Bookshop app. - + ```js sap.ui.define( [ @@ -818,7 +821,7 @@ To enable changelog viewing in your CAP application: }); }); ``` - + - Replace `books` in `ControllerExtension.extend` with the `SAPUI5.Component` name from your `app/appconfig/fioriSandboxConfig.json` file. See this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/appconfig/fioriSandboxConfig.json#L86). - Replace `AdminService` in `invokeAction("AdminService.changelog")` with the name of your service. @@ -889,7 +892,7 @@ To enable changelog viewing in your CAP application: To add a custom action button (e.g., "Change Log") to your table that is enabled only when a single row is selected, add the following configuration to your `manifest.json`. See this [example](https://github.com/cap-java/sdm/blob/396339d3182f1debe96a3134c42b17b609357d9a/cap-notebook/demoapp/app/admin-books/webapp/manifest.json#L143) from a sample Bookshop app. - + ```json "controlConfiguration": { "attachments/@com.sap.vocabularies.UI.v1.LineItem": { @@ -915,13 +918,13 @@ To enable changelog viewing in your CAP application: ``` - Replace `attachments` with your entity’s facet name as needed. - Repeat for other facets's if required. - - Replace `books` in `"press": ".extension.books.controller.custom.onChangelogPress"` with the SAPUI5.Component name from your - `app/appconfig/fioriSandboxConfig.json` file. Refer this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/appconfig/fioriSandboxConfig.json#L86) from a sample Bookshop app. + - Replace `books` in `"press": ".extension.books.controller.custom.onChangelogPress"` with the SAPUI5.Component name from your + `app/appconfig/fioriSandboxConfig.json` file. Refer this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/appconfig/fioriSandboxConfig.json#L86) from a sample Bookshop app. ### Configuration Properties | Property | Value | Description | - |----------|-------|-------------| + |----------|-------|-------------| | `enableOnSelect` | `"single"` | Button is enabled only when exactly one row is selected | | `requiresSelection` | `true` | Button is disabled when no rows are selected | | `press` | `".extension.books.controller.custom.onChangelogPress"` | Reference to the controller method that handles the button click | @@ -933,11 +936,11 @@ To enable changelog viewing in your CAP application: The button will automatically be: | Status | Condition | - |--------|-----------| + |--------|-----------| | ✅ Enabled | When exactly one item is selected | | ❌ Disabled | When no items are selected | | ❌ Disabled | When multiple items are selected | - + ## Support for link type attachments @@ -949,19 +952,19 @@ This plugin provides the capability to create, open, rename and delete attachmen ### Steps to Enable Row-Press for Open Link 1. **Add the `openAttachment` action to application's service definition** - + See this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/srv/admin-service.cds#L19) from a sample Bookshop app. ```cds action openAttachment() returns String; ``` -2. **Add a custom controller extension** +2. **Add a custom controller extension** In webapp/controller/custom.controller.js, copy and paste below content. - + See this [example](https://github.com/cap-java/sdm/blob/develop_deploy/cap-notebook/demoapp/app/admin-books/webapp/controller/custom.controller.js) from a sample Bookshop app. - + ```js sap.ui.define( [ @@ -997,7 +1000,7 @@ This plugin provides the capability to create, open, rename and delete attachmen } ); ``` - + - Replace `books` in `ControllerExtension.extend` with the `SAPUI5.Component` name from your `app/appconfig/fioriSandboxConfig.json` file. See this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/appconfig/fioriSandboxConfig.json#L86). - Replace `AdminService` in `invokeAction("AdminService.openAttachment")` with the name of your service. @@ -1018,11 +1021,11 @@ This plugin provides the capability to create, open, rename and delete attachmen ``` - Replace `attachments` with your entity’s facet name as needed. - Repeat for other facets's if required. - - Replace `books` in `"rowPress": ".extension.books.controller.custom.onRowPress"` with the SAPUI5.Component name from your - `app/appconfig/fioriSandboxConfig.json` file. Refer this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/appconfig/fioriSandboxConfig.json#L86) from a sample Bookshop app. + - Replace `books` in `"rowPress": ".extension.books.controller.custom.onRowPress"` with the SAPUI5.Component name from your + `app/appconfig/fioriSandboxConfig.json` file. Refer this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/appconfig/fioriSandboxConfig.json#L86) from a sample Bookshop app. 4. **Register the Custom Controller Extension** - + In the root of your `sap.ui5` section, add or extend the `extends` property to register your custom controller by copy and pasting below content. See this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/admin-books/webapp/manifest.json#L159) ```json @@ -1036,18 +1039,18 @@ This plugin provides the capability to create, open, rename and delete attachmen } } ``` - - Replace `books` in `"sap.fe.templates.ObjectPage.ObjectPageController#books::BooksDetailsList"` with the SAPUI5.Component name from your - `app/appconfig/fioriSandboxConfig.json` file. Refer this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/appconfig/fioriSandboxConfig.json#L86) from a sample Bookshop app. + - Replace `books` in `"sap.fe.templates.ObjectPage.ObjectPageController#books::BooksDetailsList"` with the SAPUI5.Component name from your + `app/appconfig/fioriSandboxConfig.json` file. Refer this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/appconfig/fioriSandboxConfig.json#L86) from a sample Bookshop app. - Replace `BooksDetailsList` in `"sap.fe.templates.ObjectPage.ObjectPageController#books::BooksDetailsList"` with id of the relevant Object Page (e.g., BooksDetails). Refer this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/admin-books/webapp/manifest.json#L109) from a sample Bookshop app. - - Replace `books` in `"controllerName": "books.controller.custom"` with the SAPUI5.Component name from your - `app/appconfig/fioriSandboxConfig.json` file. Refer this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/appconfig/fioriSandboxConfig.json#L86) from a sample Bookshop app. + - Replace `books` in `"controllerName": "books.controller.custom"` with the SAPUI5.Component name from your + `app/appconfig/fioriSandboxConfig.json` file. Refer this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/app/appconfig/fioriSandboxConfig.json#L86) from a sample Bookshop app. ### Steps to Enable Create Link Feature in CAP Application > **Note:** Enabling row-press for open link (see steps above) is a prerequisite for link support. -1. **Add the `createLink` action to application's service definition** - +1. **Add the `createLink` action to application's service definition** + See this [example](https://github.com/cap-java/sdm/blob/90cfc716967d844e114457a710daebdd55431965/cap-notebook/demoapp/srv/admin-service.cds#L12) from a sample Bookshop app: ```cds @@ -1149,8 +1152,8 @@ This plugin provides the capability to update/edit the URL of attachments of lin ### Steps to Enable Edit Link Feature in CAP Application -1. **Add the `editLink` action to application's service definition** - +1. **Add the `editLink` action to application's service definition** + See this [example](https://github.com/cap-java/sdm/blob/a1fc26f3aa92ffd4f9203d815f51107838d5f677/cap-notebook/demoapp/srv/admin-service.cds#L18) from a sample Bookshop app: ```cds @@ -1266,13 +1269,37 @@ SDM.userNotAuthorisedError=Sie verfügen nicht über die erforderlichen Berechti SDM.mimetypeInvalidError=Der Dateityp ist nicht zulässig SDM.maxCountErrorMessage=Maximale Anzahl von Anhängen erreicht ``` +## Support for Attachment Upload Status + +The attachment upload process displays a status indicator for each file being uploaded. + +**For repositories without virus scanning:** +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 - UI5 Version 1.135.0: This version causes error in upload of attachments. - Repository : This plugin does not support the use of versioned repositories. -- File size : 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 scanning, only attachments upto 400 MB will be scanned for virus. -- Datatypes for custom properties : Custom properties are supported for the following data types `String`, `Boolean`, `Decimal`, `Integer` and `DateTime`. +- File size : 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 scanning, only attachments upto 400 MB will be scanned for virus. +- Datatypes for custom properties : Custom properties are supported for the following data types `String`, `Boolean`, `Decimal`, `Integer` and `DateTime`. ## Support, Feedback, Contributing @@ -1284,5 +1311,4 @@ We as members, contributors, and leaders pledge to make participation in our com ## Licensing -Copyright 2024 SAP SE or an SAP affiliate company and contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/cap-java/sdm). - +Copyright 2024 SAP SE or an SAP affiliate company and contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/cap-java/sdm). \ No newline at end of file diff --git a/cap-notebook/demoapp/db/data/sap.attachments-UploadScanStates.csv b/cap-notebook/demoapp/db/data/sap.attachments-UploadScanStates.csv new file mode 100644 index 000000000..f61e426c2 --- /dev/null +++ b/cap-notebook/demoapp/db/data/sap.attachments-UploadScanStates.csv @@ -0,0 +1,6 @@ +code;name;criticality +uploading;Uploading;5 +Success;Success;3 +Failed;Scan Failed;2 +VirusDetected;Virus detected;1 +VirusScanInprogress;Virus scanning in progress(Refresh the page);5 \ No newline at end of file diff --git a/cap-notebook/demoapp/mta.yaml b/cap-notebook/demoapp/mta.yaml new file mode 100644 index 000000000..bd1db05c7 --- /dev/null +++ b/cap-notebook/demoapp/mta.yaml @@ -0,0 +1,110 @@ +_schema-version: '2.1' +ID: demoappjava +version: 1.0.0 +description: "demoappjava CAP Java Project with UI" +parameters: + enable-parallel-deployments: true +modules: +# --------------------- SERVER MODULE ------------------------ + - name: demoappjava-srv +# ------------------------------------------------------------ + type: java + path: srv + parameters: + memory: 1024M + disk-quota: 512M + buildpack: sap_java_buildpack_jakarta + properties: + SPRING_PROFILES_ACTIVE: cloud,sandbox + JBP_CONFIG_COMPONENTS: "jres: ['com.sap.xs.java.buildpack.jre.SAPMachineJRE']" + JBP_CONFIG_SAP_MACHINE_JRE: '{ version: 17.+ }' + REPOSITORY_ID: b760c15f-bd79-408d-8cac-e8b2237bb0b6 # Placeholder for REPOSITORY_ID + INCOMING_REQUEST_TIMEOUT: 3600000 + INCOMING_SESSION_TIMEOUT: 3600000 + INCOMING_CONNECTION_TIMEOUT: 3600000 + build-parameters: + builder: custom + commands: + - mvn clean package -DskipTests=true + build-result: target/*-exec.jar + requires: + - name: demoappjava-hdi-container + - name: demoappjava-public-uaa + - name: cf-logging + - name: sdm + provides: + - name: srv-api + properties: + srv-url: '${default-url}' +# --------------------- DB MODULE --------------------------- + - name: demoappjava-db +# ----------------------------------------------------------- + type: hdb + path: db + parameters: + buildpack: nodejs_buildpack + build-parameters: + builder: custom + commands: + - npm run build + requires: + - name: demoappjava-srv + requires: + - name: demoappjava-hdi-container +# --------------------- APPROUTER MODULE --------------------- + - name: demoappjava-app +# ------------------------------------------------------------ + type: approuter.nodejs + path: app + parameters: + memory: 256M + disk-quota: 512M + properties: + INCOMING_REQUEST_TIMEOUT: 3600000 + INCOMING_SESSION_TIMEOUT: 3600000 + INCOMING_CONNECTION_TIMEOUT: 3600000 + requires: + - name: srv-api + group: destinations + properties: + name: backend + url: ~{srv-url} + forwardAuthToken: true + strictSSL: true + timeout: 3600000 + - name: demoappjava-public-uaa + provides: + - name: app-api + properties: + app-url: '${default-url}' +# --------------------- RESOURCES --------------------- +resources: +# ----------------------------------------------------- + - name: demoappjava-public-uaa + type: org.cloudfoundry.managed-service + parameters: + service: xsuaa + service-plan: application + path: ./xs-security.json + config: # override xsappname as it needs to be unique + xsappname: demoappjava-${org}-${space} + oauth2-configuration: + redirect-uris: + - ~{app-api/app-url}/** + requires: + - name: app-api + - name: demoappjava-hdi-container + type: org.cloudfoundry.managed-service + parameters: + service: hana + service-plan: hdi-shared + - name: cf-logging + type: org.cloudfoundry.managed-service + parameters: + service: application-logs + service-plan: lite + - name: sdm + type: org.cloudfoundry.managed-service + parameters: + service: sdm-test + service-plan: standard diff --git a/sdm/pom.xml b/sdm/pom.xml index ed10aed4d..7acda979c 100644 --- a/sdm/pom.xml +++ b/sdm/pom.xml @@ -34,7 +34,7 @@ src/test/gen 17 17 - 1.2.2 + 1.2.4 1.18.36 0.8.7 3.10.8 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 639d67302..95966b8e0 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 @@ -73,7 +73,8 @@ public void eventHandlers(CdsRuntimeConfigurer configurer) { SDMService sdmService = new SDMServiceImpl(binding, connectionPool, tokenHandlerInstance); DocumentUploadService documentService = new DocumentUploadService(binding, connectionPool, tokenHandlerInstance); - configurer.eventHandler(buildReadHandler()); + configurer.eventHandler( + buildReadHandler(persistenceService, sdmService, tokenHandlerInstance, dbQueryInstance)); configurer.eventHandler( new SDMCreateAttachmentsHandler( persistenceService, sdmService, tokenHandlerInstance, dbQueryInstance)); @@ -125,7 +126,11 @@ private static CdsProperties.ConnectionPool getConnectionPool(CdsEnvironment env return new CdsProperties.ConnectionPool(timeout, maxConnections, maxConnections); } - protected EventHandler buildReadHandler() { - return new SDMReadAttachmentsHandler(); + protected EventHandler buildReadHandler( + PersistenceService persistenceService, + SDMService sdmService, + TokenHandler tokenHandler, + DBQuery dbQuery) { + return new SDMReadAttachmentsHandler(persistenceService, sdmService, tokenHandler, dbQuery); } } 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 9be2627c9..d58e0396a 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 @@ -1,5 +1,8 @@ package com.sap.cds.sdm.constants; +import java.util.Collection; +import java.util.List; + public class SDMConstants { private SDMConstants() { // Doesn't do anything @@ -8,10 +11,64 @@ private SDMConstants() { public static final String REPOSITORY_ID = System.getenv("REPOSITORY_ID"); public static final String MIMETYPE_INTERNET_SHORTCUT = "application/internet-shortcut"; public static final String SYSTEM_USER = "system-internal"; + public static final String SDM_READONLY_CONTEXT = "SDM_READONLY_CONTEXT"; + public static final String SDM_ANNOTATION_ADDITIONALPROPERTY_NAME = "SDM.Attachments.AdditionalProperty.name"; 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 = + "The following files could not be renamed as they already exist:\n%s\n"; + public static final String COULD_NOT_UPDATE_THE_ATTACHMENT = "Could not update the attachment"; + public static final String ATTACHMENT_NOT_FOUND = "Attachment not found"; + public static final String GENERIC_ERROR = "Could not %s the document."; + public static final String VERSIONED_REPO_ERROR = + "Upload not supported for versioned repositories."; + public static final String VIRUS_REPO_ERROR_MORE_THAN_400MB = + "You cannot upload files that are larger than 400 MB"; + public static final String VIRUS_REPO_ERROR_MORE_THAN_400MB_MESSAGE = "SDM.VirusRepoErrorMessage"; + public static final String VIRUS_ERROR = "%s contains potential malware and cannot be uploaded."; + public static final String VIRUS_ERROR_MESSAGE = "SDM.VirusErrorMessage"; + public static final String SDM_DUPLICATE_ATTACHMENT = "SDM.DuplicateAttachment"; + public static final String REPOSITORY_ERROR = "Failed to get repository info."; + public static final String SDM_MISSING_ROLES_EXCEPTION_MSG = + "You do not have the required permissions to update attachments. Kindly contact the admin"; + public static final String SDM_ROLES_ERROR_MESSAGE = + "Unable to rename the file due to an error at the server"; + public static final String SDM_ENV_NAME = "sdm"; + public static final String ENTITY_PROCESSING_ERROR_LINK = + "Failed to create link due to error while processing entity"; + public static final String SDM_TOKEN_EXCHANGE_DESTINATION = "sdm-token-exchange-flow"; + public static final String SDM_TECHNICAL_CREDENTIALS_FLOW_DESTINATION = "sdm-technical-user-flow"; + public static final String SDM_TOKEN_FETCH = "sdm-token-fetch"; + public static final String SDM_DESTINATION_KEY = "name"; + public static final String SDM_CONNECTIONPOOL_PREFIX = "cds.attachments.sdm.http.%s"; + public static final String USER_NOT_AUTHORISED_ERROR = + "You do not have the required permissions to upload attachments. Please contact your administrator for access."; + public static final String MIMETYPE_INVALID_ERROR = + "This file type is not allowed in this repository. Contact your administrator for assistance."; + public static final String USER_NOT_AUTHORISED_ERROR_LINK = + "You do not have the required permissions to create links. Please contact your administrator for access."; + public static final String USER_NOT_AUTHORISED_ERROR_OPEN_LINK = + "You do not have the required permissions to open links. Please contact your administrator for access."; + public static final String FILE_NOT_FOUND_ERROR = "Object not found in repository"; + public static final Integer MAX_CONNECTIONS = 100; + public static final int CONNECTION_TIMEOUT = 1200; + public static final int CHUNK_SIZE = 20 * 1024 * 1024; // 20MB Chunk Size + public static final String ONBOARD_REPO_MESSAGE = + "Repository with name %s and id %s onboarded successfully"; + public static final String REPOSITORY_ALREADY_EXIST = + "Repository with name %s and id %s already exists. Skipping onboarding."; + 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 final String ATTACHMENT_MAXCOUNT = "SDM.Attachments.maxCount"; + public static final String ATTACHMENT_MAXCOUNT_ERROR_MSG = "SDM.Attachments.maxCountError"; + public static final String MAX_COUNT_ERROR_MESSAGE = + "Cannot upload more than %s attachments as set up by the application"; + public static final String FETCH_CHANGELOG_ERROR = "Could not fetch the changelog"; public static final String DRAFT_READONLY_CONTEXT = "DRAFT_READONLY_CONTEXT"; public static final Integer TIMEOUT_MILLISECONDS = 900000; public static final Integer MAX_CONNECTIONS_PER_ROUTE = 50; @@ -20,14 +77,178 @@ private SDMConstants() { public static final String TECHNICAL_USER_FLOW = "TECHNICAL_CREDENTIALS_FLOW"; public static final String NAMED_USER_FLOW = "TOKEN_EXCHANGE"; public static final String ANNOTATION_IS_MEDIA_DATA = "_is_media_data"; - public static final Integer MAX_CONNECTIONS = 100; - public static final int CONNECTION_TIMEOUT = 1200; - public static final int CHUNK_SIZE = 20 * 1024 * 1024; // 20MB Chunk Size - public static final String SDM_ENV_NAME = "sdm"; - public static final String SDM_TOKEN_EXCHANGE_DESTINATION = "sdm-token-exchange-flow"; - public static final String SDM_TECHNICAL_CREDENTIALS_FLOW_DESTINATION = "sdm-technical-user-flow"; - public static final String SDM_TOKEN_FETCH = "sdm-token-fetch"; - public static final String SDM_DESTINATION_KEY = "name"; - public static final String SDM_CONNECTIONPOOL_PREFIX = "cds.attachments.sdm.http.%s"; - public static final String ATTACHMENT_MAXCOUNT = "SDM.Attachments.maxCount"; + public static final String FAILED_TO_COPY_ATTACHMENT = "Failed to copy attachment"; + + // Error messages for move operations + public static final String SDM_MOVE_OPERATION_FAILED = "SDM move operation failed"; + public static final String VALIDATION_FAILED_PREFIX = "Validation failed: "; + public static final String VALIDATION_FAILED_DEFAULT_MESSAGE = + "Validation failed: Unable to process attachment properties or metadata"; + public static final String INVALID_SECONDARY_PROPERTIES_PREFIX = + "Invalid secondary properties detected: "; + public static final String INVALID_SECONDARY_PROPERTIES_SUFFIX = + ". Attachment rolled back to source."; + public static final String FAILED_TO_MOVE_ATTACHMENT = "Failed to move attachment"; + public static final String FAILED_TO_MOVE_ATTACHMENT_MSG = "SDM.Move.failedToMoveAttachmentError"; + public static final String MOVE_OPERATION_PARTIAL_FAILURE = + "Move operation completed with some failures"; + public static final String FAILED_TO_FETCH_UP_ID = "Failed to fetch up_id"; + public static final String FAILED_TO_FETCH_FACET = + "Invalid facet format, unable to extract required information."; + public static final String PARENT_ENTITY_NOT_FOUND_ERROR = "Unable to find parent entity: %s"; + public static final String COMPOSITION_NOT_FOUND_ERROR = + "Unable to find composition '%s' in entity: %s"; + public static final String TARGET_ATTACHMENT_ENTITY_NOT_FOUND_ERROR = + "Unable to find target attachment entity: %s"; + + public static final String SINGLE_RESTRICTED_CHARACTER_IN_FILE = + "\"%s\" contains unsupported characters (‘/’ or ‘\\’). Rename and try again."; + public static final String SINGLE_DUPLICATE_FILENAME = + "An object named \"%s\" already exists. Rename the object and try again."; + public static final String VIRUS_DETECTED_ERROR_MSG = + "You can't save your changes because some files are unsafe. Delete the unsafe files manually before continuing. You can use a filter to help you find the affected files."; + public static final String SCAN_FAILED_ERROR_MSG = + "You can't save your changes because some files not scanned. Delete the unscanned files manually before continuing."; + public static final String VIRUS_SCAN_IN_PROGRESS_ERROR_MSG = + "Refresh the page to see scanning is completed."; + + // Upload Status Constants + public static final String UPLOAD_STATUS_SUCCESS = "Success"; + public static final String UPLOAD_STATUS_VIRUS_DETECTED = "VirusDetected"; + public static final String UPLOAD_STATUS_IN_PROGRESS = "uploading"; + public static final String UPLOAD_STATUS_FAILED = "Failed"; + public static final String UPLOAD_STATUS_SCAN_FAILED = "Failed"; + public static final String VIRUS_SCAN_INPROGRESS = "VirusScanInprogress"; + + // New scan status constants + + public enum ScanStatus { + BLANK(""), + PENDING("PENDING"), + SCANNING("SCANNING"), + CLEAN("CLEAN"), + QUARANTINED("QUARANTINED"), + FAILED("FAILED"); + + private final String value; + + ScanStatus(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static ScanStatus fromValue(String value) { + if (value == null || value.trim().isEmpty()) { + return BLANK; + } + for (ScanStatus status : values()) { + if (status.value.equalsIgnoreCase(value)) { + return status; + } + } + return BLANK; // Default to blank for unknown values + } + } + + // Helper Methods to create error/warning messages + public static String buildErrorMessage( + Collection filenames, StringBuilder prefixTemplate, String closingRemark) { + for (String file : filenames) { + prefixTemplate.append(String.format("\t• %s%n", file)); + } + if (closingRemark != null && !closingRemark.isEmpty()) + prefixTemplate.append("\n ").append(closingRemark); + return prefixTemplate.toString(); + } + + // Restricted characters: / and \ + public static String nameConstraintMessage(List invalidFileNames) { + // if only 1 restricted character is there in file, so different error will throw + if (invalidFileNames.size() == 1) { + return String.format(SINGLE_RESTRICTED_CHARACTER_IN_FILE, invalidFileNames.iterator().next()); + } + StringBuilder prefix = new StringBuilder(); + prefix.append( + "The following names contain unsupported characters (‘/’ or ‘\\’). Rename and try again:\n\n"); + return buildErrorMessage(invalidFileNames, prefix, null); + } + + // Duplicate file names error message + public static String duplicateFilenameFormat(Collection duplicateFileNames) { + // if only 1 duplicate file, so different error will throw + if (duplicateFileNames.size() == 1) { + return String.format(SINGLE_DUPLICATE_FILENAME, duplicateFileNames.iterator().next()); + } + StringBuilder prefix = new StringBuilder(); + prefix.append("Objects with the following names already exist:\n\n"); + String closingRemark = "Rename the objects and try again"; + return buildErrorMessage(duplicateFileNames, prefix, closingRemark); + } + + public static String fileNotFound(List fileNameNotFound) { + // Create the base message + String prefixMessage = + "Update unsuccessful. The following filename(s) could not be updated as they do not exist. \n\n"; + + // Create the formatted prefix message + String formattedPrefixMessage = String.format(prefixMessage); + + // Initialize the StringBuilder with the formatted message prefix + StringBuilder bulletPoints = new StringBuilder(formattedPrefixMessage); + + // Append each unsupported file name to the StringBuilder + for (String file : fileNameNotFound) { + bulletPoints.append(String.format("\t• %s%n", file)); + } + bulletPoints.append("\nDelete and upload the files again."); + return bulletPoints.toString(); + } + + public static String noSDMRolesMessage(List files, String operation) { + // Create the base message + String prefixMessage = "Could not " + operation + " the following files. \n\n"; + + // Initialize the StringBuilder with the formatted message prefix + StringBuilder bulletPoints = new StringBuilder(prefixMessage); + + // Append each file name and its error message to the StringBuilder + for (String item : files) { + bulletPoints.append(String.format("\t• %s%n", item)); + } + bulletPoints.append(System.lineSeparator()); + if (operation.equals("create")) { + bulletPoints.append(USER_NOT_AUTHORISED_ERROR); + } else { + bulletPoints.append(SDM_MISSING_ROLES_EXCEPTION_MSG); + } + + return bulletPoints.toString(); + } + + public static String unsupportedPropertiesMessage(List propertiesList) { + // 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 : propertiesList) { + 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 getGenericError(String event) { + return String.format(GENERIC_ERROR, event); + } + + public static String getVirusFilesError(String filename) { + return String.format(VIRUS_ERROR, filename); + } } diff --git a/sdm/src/main/java/com/sap/cds/sdm/constants/SDMErrorKeys.java b/sdm/src/main/java/com/sap/cds/sdm/constants/SDMErrorKeys.java index d716b471e..c72dae767 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/constants/SDMErrorKeys.java +++ b/sdm/src/main/java/com/sap/cds/sdm/constants/SDMErrorKeys.java @@ -81,6 +81,19 @@ private SDMErrorKeys() { public static final String SINGLE_RESTRICTED_CHARACTER_IN_FILE_KEY = "SDM.singleRestrictedCharacterInFile"; public static final String SINGLE_DUPLICATE_FILENAME_KEY = "SDM.singleDuplicateFilename"; + public static final String VIRUS_DETECTED_FILE_ERROR_KEY = "SDM.virusDetectedFileError"; + public static final String VIRUS_SCAN_IN_PROGRESS_FILE_ERROR_KEY = + "SDM.virusScanInProgressFileError"; + + public static final String UPLOAD_IN_PROGRESS_FILE_ERROR_KEY = "SDM.uploadInProgressFileError"; + public static final String VIRUS_DETECTED_FILES_PREFIX_KEY = "SDM.virusDetectedFilesPrefix"; + public static final String VIRUS_DETECTED_FILES_SUFFIX_KEY = "SDM.virusDetectedFilesSuffix"; + public static final String VIRUS_SCAN_IN_PROGRESS_FILES_PREFIX_KEY = + "SDM.virusScanInProgressFilesPrefix"; + public static final String VIRUS_SCAN_IN_PROGRESS_FILES_SUFFIX_KEY = + "SDM.virusScanInProgressFilesSuffix"; + public static final String SCAN_FAILED_FILES_PREFIX_KEY = "SDM.scanFailedFilesPrefix"; + public static final String SCAN_FAILED_FILES_SUFFIX_KEY = "SDM.scanFailedFilesSuffix"; public static final String RESTRICTED_CHARACTERS_IN_MULTIPLE_FILES_KEY = "SDM.restrictedCharactersInMultipleFiles"; public static final String MULTIPLE_DUPLICATE_FILENAMES_PREFIX_KEY = diff --git a/sdm/src/main/java/com/sap/cds/sdm/constants/SDMErrorMessages.java b/sdm/src/main/java/com/sap/cds/sdm/constants/SDMErrorMessages.java index 60256b1c0..e43f6e99c 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/constants/SDMErrorMessages.java +++ b/sdm/src/main/java/com/sap/cds/sdm/constants/SDMErrorMessages.java @@ -96,6 +96,26 @@ private SDMErrorMessages() { "\"%s\" contains unsupported characters (‘/’ or ‘\\’). Rename and try again."; public static final String SINGLE_DUPLICATE_FILENAME = "An object named \"%s\" already exists. Rename the object and try again."; + public static final String VIRUS_DETECTED_FILE_ERROR = + "Virus detected in this file kindly delete it."; + public static final String VIRUS_SCAN_IN_PROGRESS_FILE_ERROR = + "Virus scanning is in progress. Refresh the page to see the effect."; + public static final String VIRUS_DETECTED_FILES_PREFIX = + "We detected a virus, for the following files: \n\n"; + public static final String VIRUS_DETECTED_FILES_SUFFIX = + "You can't save your changes because some files are unsafe. Delete the unsafe files manually before continuing. You can use a filter to help you find the affected files."; + public static final String VIRUS_SCAN_IN_PROGRESS_FILES_PREFIX = + "The virus scanning is in progress for the following files: \n\n"; + public static final String VIRUS_SCAN_IN_PROGRESS_FILES_SUFFIX = + "Refresh the page to see scanning is completed."; + public static final String SCAN_FAILED_FILES_PREFIX = + "The virus scan failed, for the following files: \n\n"; + public static final String SCAN_FAILED_FILES_SUFFIX = + "You can't save your changes because some files not scanned. Delete the unscanned files manually before continuing."; + public static final String UPLOAD_IN_PROGRESS_FILES_PREFIX = + "The upload is in progress for the following files: \n\n"; + public static final String UPLOAD_IN_PROGRESS_FILES_SUFFIX = + "You can't save your changes until the upload completes. Refresh the page to check if the upload is complete."; public static final String RESTRICTED_CHARACTERS_IN_MULTIPLE_FILES = "The following names contain unsupported characters (‘/’ or ‘\\’). Rename and try again:\n\n"; public static final String MULTIPLE_DUPLICATE_FILENAMES_PREFIX = @@ -251,6 +271,34 @@ public static String getVirusFilesError(String filename) { return String.format(SDMUtils.getErrorMessage("VIRUS_ERROR"), filename); } + public static String virusDetectedFilesMessage(List files) { + StringBuilder prefix = new StringBuilder(); + prefix.append(SDMUtils.getErrorMessage("VIRUS_DETECTED_FILES_PREFIX")); + String closingRemark = SDMUtils.getErrorMessage("VIRUS_DETECTED_FILES_SUFFIX"); + return buildErrorMessage(files, prefix, closingRemark); + } + + public static String scanFailedFilesMessage(List files) { + StringBuilder prefix = new StringBuilder(); + prefix.append(SDMUtils.getErrorMessage("SCAN_FAILED_FILES_PREFIX")); + String closingRemark = SDMUtils.getErrorMessage("SCAN_FAILED_FILES_SUFFIX"); + return buildErrorMessage(files, prefix, closingRemark); + } + + public static String virusScanInProgressFilesMessage(List files) { + StringBuilder prefix = new StringBuilder(); + prefix.append(SDMUtils.getErrorMessage("VIRUS_SCAN_IN_PROGRESS_FILES_PREFIX")); + String closingRemark = SDMUtils.getErrorMessage("VIRUS_SCAN_IN_PROGRESS_FILES_SUFFIX"); + return buildErrorMessage(files, prefix, closingRemark); + } + + public static String uploadInProgressFilesMessage(List files) { + StringBuilder prefix = new StringBuilder(); + prefix.append(SDMUtils.getErrorMessage("UPLOAD_IN_PROGRESS_FILES_PREFIX")); + String closingRemark = SDMUtils.getErrorMessage("UPLOAD_IN_PROGRESS_FILES_SUFFIX"); + return buildErrorMessage(files, prefix, closingRemark); + } + public static Map getAllErrorMessages() { Map out = new LinkedHashMap<>(); for (Field f : SDMErrorMessages.class.getDeclaredFields()) { 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 d93ad757c..5c5796437 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,5 +1,7 @@ package com.sap.cds.sdm.handler.applicationservice; +import static com.sap.cds.sdm.constants.SDMConstants.SDM_READONLY_CONTEXT; + import com.sap.cds.CdsData; import com.sap.cds.reflect.CdsEntity; import com.sap.cds.sdm.caching.CacheConfig; @@ -17,10 +19,12 @@ import com.sap.cds.services.cds.ApplicationService; import com.sap.cds.services.cds.CdsCreateEventContext; import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.After; 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 com.sap.cds.services.utils.OrderConstants; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -29,6 +33,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,9 +57,10 @@ public SDMCreateAttachmentsHandler( } @Before - @HandlerOrder(HandlerOrder.EARLY) + @HandlerOrder(HandlerOrder.DEFAULT) public void processBefore(CdsCreateEventContext context, List data) throws IOException { logger.info("Target Entity : " + context.getTarget().getQualifiedName()); + for (CdsData entityData : data) { Map> attachmentCompositionDetails = AttachmentsHandlerUtils.getAttachmentCompositionDetails( @@ -65,9 +71,68 @@ public void processBefore(CdsCreateEventContext context, List data) thr entityData); logger.info("Attachment compositions present in CDS Model : " + attachmentCompositionDetails); updateName(context, data, attachmentCompositionDetails); + // Remove uploadStatus from attachment data to prevent validation errors + + } + SDMUtils.cleanupReadonlyContexts(data); + } + + @After + @HandlerOrder(HandlerOrder.LATE) + public void processAfter(CdsCreateEventContext context, List data) { + // Update uploadStatus to Success after entity is persisted + logger.info( + "Post-processing attachments after persistence for entity: {}", + context.getTarget().getQualifiedName()); + + for (CdsData entityData : data) { + Map> attachmentCompositionDetails = + AttachmentsHandlerUtils.getAttachmentCompositionDetails( + context.getModel(), + context.getTarget(), + persistenceService, + context.getTarget().getQualifiedName(), + entityData); + + for (Map.Entry> entry : attachmentCompositionDetails.entrySet()) { + String attachmentCompositionDefinition = entry.getKey(); + String attachmentCompositionName = entry.getValue().get("name"); + Optional attachmentEntity = + context.getModel().findEntity(attachmentCompositionDefinition); + + if (attachmentEntity.isPresent()) { + String targetEntity = context.getTarget().getQualifiedName(); + List> attachments = + AttachmentsHandlerUtils.fetchAttachments( + targetEntity, entityData, attachmentCompositionName); + + if (attachments != null) { + for (Map attachment : attachments) { + String id = (String) attachment.get("ID"); + String uploadStatus = (String) attachment.get("uploadStatus"); + if (id != null) { + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setAttachmentId(id); + cmisDocument.setUploadStatus(uploadStatus); + // Update uploadStatus to Success in database if it was InProgress + dbQuery.saveUploadStatusToAttachment( + attachmentEntity.get(), persistenceService, cmisDocument); + logger.debug("Updated uploadStatus to Success for attachment ID: {}", id); + } + } + } + } + } } } + @Before + @HandlerOrder(OrderConstants.Before.CHECK_CAPABILITIES - 500) + public void preserveUploadStatus(CdsCreateEventContext context, List data) { + // Preserve uploadStatus before CDS removes readonly fields + SDMUtils.preserveReadonlyFields(context.getTarget(), data); + } + public void updateName( CdsCreateEventContext context, List data, @@ -163,6 +228,11 @@ private void processEntity( String targetEntity = context.getTarget().getQualifiedName(); List> attachments = AttachmentsHandlerUtils.fetchAttachments(targetEntity, entity, attachmentCompositionName); + List virusDetectedFiles = new ArrayList<>(); + List virusScanInProgressFiles = new ArrayList<>(); + List scanFailedFiles = new ArrayList<>(); + List uploadInProgressFiles = new ArrayList<>(); + if (attachments != null) { for (Map attachment : attachments) { processAttachment( @@ -176,8 +246,21 @@ private void processEntity( composition, attachmentEntity, secondaryPropertiesWithInvalidDefinitions, - noSDMRoles); + noSDMRoles, + virusDetectedFiles, + virusScanInProgressFiles, + scanFailedFiles, + uploadInProgressFiles); } + + // Throw exception if any files failed virus scan or scan failed + String errorMessage = + buildErrorMessage( + virusDetectedFiles, virusScanInProgressFiles, scanFailedFiles, uploadInProgressFiles); + if (!errorMessage.isEmpty()) { + throw new ServiceException(errorMessage); + } + SecondaryPropertiesKey secondaryPropertiesKey = new SecondaryPropertiesKey(); // Emptying cache after attachments are updated in loop secondaryPropertiesKey.setRepositoryId(SDMConstants.REPOSITORY_ID); @@ -196,23 +279,136 @@ private void processAttachment( String composition, Optional attachmentEntity, Map secondaryPropertiesWithInvalidDefinitions, - List noSDMRoles) + List noSDMRoles, + List virusDetectedFiles, + List virusScanInProgressFiles, + List scanFailedFiles, + List uploadInProgressFiles) throws IOException { String id = (String) attachment.get("ID"); String filenameInRequest = (String) attachment.get("fileName"); String descriptionInRequest = (String) attachment.get("note"); String objectId = (String) attachment.get("objectId"); - // Fetch original data from DB and SDM - String fileNameInDB = + // Fetch original data from DB + CmisDocument cmisDocument = dbQuery.getAttachmentForID(attachmentEntity.get(), persistenceService, id); + String fileNameInDB = cmisDocument.getFileName(); + + // Check upload status and collect problematic files + if (checkUploadStatus( + attachment, + fileNameInDB, + filenameInRequest, + virusDetectedFiles, + virusScanInProgressFiles, + scanFailedFiles, + uploadInProgressFiles)) { + return; // Skip further processing if upload status is problematic + } + + // Fetch data from SDM SDMCredentials sdmCredentials = tokenHandler.getSDMCredentials(); - List sdmAttachmentData = + SDMAttachmentData sdmData = fetchSDMData(context, objectId, sdmCredentials); + + // Prepare and update attachment in SDM + updateAndSendToSDM( + context, + attachment, + id, + objectId, + filenameInRequest, + descriptionInRequest, + fileNameInDB, + sdmData.fileNameInSDM, + sdmData.descriptionInSDM, + sdmCredentials, + attachmentEntity, + secondaryPropertiesWithInvalidDefinitions, + noSDMRoles, + duplicateFileNameList, + filesNotFound, + filesWithUnsupportedProperties, + badRequest); + } + + private boolean checkUploadStatus( + Map attachment, + String fileNameInDB, + String filenameInRequest, + List virusDetectedFiles, + List virusScanInProgressFiles, + List scanFailedFiles, + List uploadInProgressFiles) { + Map readonlyData = (Map) attachment.get(SDM_READONLY_CONTEXT); + if (readonlyData == null || readonlyData.get("uploadStatus") == null) { + return false; + } + + String uploadStatus = readonlyData.get("uploadStatus").toString(); + String fileName = fileNameInDB != null ? fileNameInDB : filenameInRequest; + + if (uploadStatus.equalsIgnoreCase(SDMConstants.UPLOAD_STATUS_VIRUS_DETECTED)) { + virusDetectedFiles.add(fileName); + return true; + } + if (uploadStatus.equalsIgnoreCase(SDMConstants.VIRUS_SCAN_INPROGRESS)) { + virusScanInProgressFiles.add(fileName); + return true; + } + if (uploadStatus.equalsIgnoreCase(SDMConstants.UPLOAD_STATUS_IN_PROGRESS)) { + uploadInProgressFiles.add(fileName); + return true; + } + if (uploadStatus.equalsIgnoreCase(SDMConstants.UPLOAD_STATUS_SCAN_FAILED)) { + scanFailedFiles.add(fileName); + return true; + } + + attachment.put("uploadStatus", uploadStatus); + return false; + } + + private SDMAttachmentData fetchSDMData( + CdsCreateEventContext context, String objectId, SDMCredentials sdmCredentials) + throws IOException { + JSONObject sdmAttachmentData = AttachmentsHandlerUtils.fetchAttachmentDataFromSDM( sdmService, objectId, sdmCredentials, context.getUserInfo().isSystemUser()); - String fileNameInSDM = sdmAttachmentData.get(0); - String descriptionInSDM = sdmAttachmentData.get(1); + JSONObject succinctProperties = sdmAttachmentData.getJSONObject("succinctProperties"); + + String fileNameInSDM = null; + String descriptionInSDM = null; + if (succinctProperties.has("cmis:name")) { + fileNameInSDM = succinctProperties.getString("cmis:name"); + } + if (succinctProperties.has("cmis:description")) { + descriptionInSDM = succinctProperties.getString("cmis:description"); + } + + return new SDMAttachmentData(fileNameInSDM, descriptionInSDM); + } + + private void updateAndSendToSDM( + CdsCreateEventContext context, + Map attachment, + String id, + String objectId, + String filenameInRequest, + String descriptionInRequest, + String fileNameInDB, + String fileNameInSDM, + String descriptionInSDM, + SDMCredentials sdmCredentials, + Optional attachmentEntity, + Map secondaryPropertiesWithInvalidDefinitions, + List noSDMRoles, + List duplicateFileNameList, + List filesNotFound, + List filesWithUnsupportedProperties, + Map badRequest) + throws IOException { Map secondaryTypeProperties = SDMUtils.getSecondaryTypeProperties(attachmentEntity, attachment); Map propertiesInDB = @@ -221,7 +417,6 @@ private void processAttachment( logger.debug("Processing attachment creation - ID: {}, objectId: {}", id, objectId); - // Prepare document and updated properties Map updatedSecondaryProperties = SDMUtils.getUpdatedSecondaryProperties( attachmentEntity, @@ -229,12 +424,10 @@ private void processAttachment( persistenceService, secondaryTypeProperties, propertiesInDB); - CmisDocument cmisDocument = AttachmentsHandlerUtils.prepareCmisDocument( filenameInRequest, descriptionInRequest, objectId); - // Update filename and description properties AttachmentsHandlerUtils.updateFilenameProperty( fileNameInDB, filenameInRequest, fileNameInSDM, updatedSecondaryProperties); AttachmentsHandlerUtils.updateDescriptionProperty( @@ -244,7 +437,6 @@ private void processAttachment( updatedSecondaryProperties, false); - // Send update to SDM and handle response logger.debug( "Creating attachment in SDM - ID: {}, properties count: {}", id, @@ -345,4 +537,47 @@ private void handleWarnings( + contextInfo); } } + + private String buildErrorMessage( + List virusDetectedFiles, + List virusScanInProgressFiles, + List scanFailedFiles, + List uploadInProgressFiles) { + StringBuilder errorMessage = new StringBuilder(); + + if (!virusDetectedFiles.isEmpty()) { + errorMessage.append(SDMErrorMessages.virusDetectedFilesMessage(virusDetectedFiles)); + } + if (!virusScanInProgressFiles.isEmpty()) { + appendWithSpace(errorMessage); + errorMessage.append( + SDMErrorMessages.virusScanInProgressFilesMessage(virusScanInProgressFiles)); + } + if (!scanFailedFiles.isEmpty()) { + appendWithSpace(errorMessage); + errorMessage.append(SDMErrorMessages.scanFailedFilesMessage(scanFailedFiles)); + } + if (!uploadInProgressFiles.isEmpty()) { + appendWithSpace(errorMessage); + errorMessage.append(SDMErrorMessages.uploadInProgressFilesMessage(uploadInProgressFiles)); + } + + return errorMessage.toString(); + } + + private void appendWithSpace(StringBuilder sb) { + if (sb.length() > 0) { + sb.append(" "); + } + } + + private static class SDMAttachmentData { + final String fileNameInSDM; + final String descriptionInSDM; + + SDMAttachmentData(String fileNameInSDM, String descriptionInSDM) { + this.fileNameInSDM = fileNameInSDM; + this.descriptionInSDM = descriptionInSDM; + } + } } 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 907ac76d7..f740a2002 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 @@ -4,52 +4,65 @@ import com.sap.cds.ql.Predicate; import com.sap.cds.ql.cqn.CqnSelect; import com.sap.cds.ql.cqn.Modifier; +import com.sap.cds.reflect.CdsAssociationType; +import com.sap.cds.reflect.CdsElementDefinition; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; import com.sap.cds.sdm.caching.CacheConfig; import com.sap.cds.sdm.caching.ErrorMessageKey; import com.sap.cds.sdm.constants.SDMConstants; import com.sap.cds.sdm.constants.SDMErrorKeys; import com.sap.cds.sdm.constants.SDMErrorMessages; +import com.sap.cds.sdm.handler.TokenHandler; +import com.sap.cds.sdm.handler.applicationservice.helper.SDMBeforeReadItemsModifier; +import com.sap.cds.sdm.handler.common.SDMApplicationHandlerHelper; +import com.sap.cds.sdm.model.CmisDocument; +import com.sap.cds.sdm.model.RepoValue; +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.cds.ApplicationService; import com.sap.cds.services.cds.CdsReadEventContext; +import com.sap.cds.services.draft.Drafts; import com.sap.cds.services.handler.EventHandler; 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.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.stream.Collectors; import org.ehcache.Cache; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @ServiceName(value = "*", type = ApplicationService.class) public class SDMReadAttachmentsHandler implements EventHandler { - public SDMReadAttachmentsHandler() {} + private static final Logger logger = LoggerFactory.getLogger(SDMReadAttachmentsHandler.class); - @Before - @HandlerOrder(HandlerOrder.DEFAULT) - public void processBefore(CdsReadEventContext context) { - String repositoryId = SDMConstants.REPOSITORY_ID; - if (context.getTarget().getAnnotationValue(SDMConstants.ANNOTATION_IS_MEDIA_DATA, false)) { - CqnSelect copy = - CQL.copy( - context.getCqn(), - new Modifier() { - @Override - public Predicate where(Predicate where) { - return CQL.and(where, CQL.get("repositoryId").eq(repositoryId)); - } - }); - setErrorMessagesInCache(context); - context.setCqn(copy); - - } else { - context.setCqn(context.getCqn()); - } + private final PersistenceService persistenceService; + private final SDMService sdmService; + private final TokenHandler tokenHandler; + private final DBQuery dbQuery; + + public SDMReadAttachmentsHandler( + PersistenceService persistenceService, + SDMService sdmService, + TokenHandler tokenHandler, + DBQuery dbQuery) { + this.persistenceService = persistenceService; + this.sdmService = sdmService; + this.tokenHandler = tokenHandler; + this.dbQuery = dbQuery; } - /* - Error message caching requires the CAP context to retrieve localized messages, which may not be - available at all error throw sites. To ensure availability, error messages are cached during the - before read event when the context is guaranteed to be present. - */ private void setErrorMessagesInCache(CdsReadEventContext context) { // Check if cache is available Cache errorMessageCache = CacheConfig.getErrorMessageCache(); @@ -91,4 +104,212 @@ private void setErrorMessagesInCache(CdsReadEventContext context) { // Mark that localized error messages have been cached errorMessageCache.put(cacheCheckKey, "true"); } + + @Before + @HandlerOrder(HandlerOrder.EARLY + 500) + public void processBefore(CdsReadEventContext context) throws IOException { + String repositoryId = SDMConstants.REPOSITORY_ID; + if (context.getTarget().getAnnotationValue(SDMConstants.ANNOTATION_IS_MEDIA_DATA, false)) { + try { + // update the uploadStatus of all blank attachments with success this is for existing + // attachments + RepoValue repoValue = + sdmService.checkRepositoryType(repositoryId, context.getUserInfo().getTenant()); + Optional attachmentDraftEntity = + context.getModel().findEntity(context.getTarget().getQualifiedName() + "_drafts"); + + if (attachmentDraftEntity.isPresent()) { + String upIdKey = SDMUtils.getUpIdKey(attachmentDraftEntity.get()); + CqnSelect select = (CqnSelect) context.get("cqn"); + String upID = SDMUtils.fetchUPIDFromCQN(select, attachmentDraftEntity.get()); + + if (!repoValue.getIsAsyncVirusScanEnabled()) { + dbQuery.updateInProgressUploadStatusToSuccess( + attachmentDraftEntity.get(), persistenceService, upID, upIdKey); + } + if (repoValue.getIsAsyncVirusScanEnabled()) { + processVirusScanInProgressAttachments(context, upID, upIdKey); + } + } + + // Get attachment associations to handle deep reads with expand + CdsModel cdsModel = context.getModel(); + List fieldNames = + getAttachmentAssociations(cdsModel, context.getTarget(), "", new ArrayList<>()); + + // Use the new modifier to handle expand scenarios + CqnSelect modifiedCqn = + CQL.copy(context.getCqn(), new SDMBeforeReadItemsModifier(fieldNames)); + + // Only add repositoryId filter if this is a collection read (no keys specified) + CqnSelect select = (CqnSelect) context.get("cqn"); + boolean hasKeys = select.ref() != null && select.ref().rootSegment().filter() != null; + + if (!hasKeys) { + // Apply repositoryId filter for collection reads + modifiedCqn = + CQL.copy( + modifiedCqn, + new Modifier() { + @Override + public Predicate where(Predicate where) { + return CQL.and(where, CQL.get("repositoryId").eq(repositoryId)); + } + }); + } + setErrorMessagesInCache(context); + context.setCqn(modifiedCqn); + } catch (Exception e) { + logger.error("Error in SDMReadAttachmentsHandler.processBefore: {}", e.getMessage(), e); + // Re-throw to maintain error handling behavior + throw e; + } + } + // No action needed for non-media entities + } + + /** + * Recursively get all attachment associations in the entity tree. This is needed to properly + * handle deep navigation like Books/covers with $expand=statusNav + */ + private List getAttachmentAssociations( + CdsModel model, CdsEntity entity, String associationName, List processedEntities) { + List associationNames = new ArrayList<>(); + if (SDMApplicationHandlerHelper.isMediaEntity(entity)) { + associationNames.add(associationName); + } + + Map annotatedEntities = + entity + .associations() + .collect( + Collectors.toMap( + CdsElementDefinition::getName, + element -> element.getType().as(CdsAssociationType.class).getTarget())); + + if (annotatedEntities.isEmpty()) { + return associationNames; + } + + for (Entry associatedElement : annotatedEntities.entrySet()) { + if (!associationNames.contains(associatedElement.getKey()) + && !processedEntities.contains(associatedElement.getKey()) + && !Drafts.SIBLING_ENTITY.equals(associatedElement.getKey())) { + processedEntities.add(associatedElement.getKey()); + List result = + getAttachmentAssociations( + model, associatedElement.getValue(), associatedElement.getKey(), processedEntities); + associationNames.addAll(result); + } + } + return associationNames; + } + + /** + * Processes attachment data and sets criticality values based on upload status. Java equivalent + * of the frontend JavaScript logic. This method will be called after data is read to enhance it + * with criticality values. + * + * @param context the CDS read event context containing attachment data + */ + private void processVirusScanInProgressAttachments( + CdsReadEventContext context, String upID, String upIDkey) { + try { + // Get the statuses of existing attachments and assign color code + // Get all attachments with virus scan in progress + Optional attachmentDraftEntity = + context.getModel().findEntity(context.getTarget().getQualifiedName() + "_drafts"); + + List attachmentsInProgress = + dbQuery.getAttachmentsWithVirusScanInProgress( + attachmentDraftEntity.get(), persistenceService, upID, upIDkey); + + // Get SDM credentials + var sdmCredentials = tokenHandler.getSDMCredentials(); + + // Iterate through each attachment and call getObject + for (CmisDocument attachment : attachmentsInProgress) { + processAttachmentVirusScanStatus( + attachment, sdmCredentials, attachmentDraftEntity.get(), persistenceService); + } + + if (!attachmentsInProgress.isEmpty()) { + logger.info( + "Processed {} attachments with virus scan in progress", attachmentsInProgress.size()); + } + + } catch (Exception e) { + logger.error("Error processing virus scan in progress attachments: {}", e.getMessage()); + } + } + + /** + * Processes a single attachment to check and update its virus scan status. + * + * @param attachment the attachment document to process + * @param sdmCredentials the SDM credentials for API calls + * @param attachmentDraftEntity the draft entity for the attachment + * @param persistenceService the persistence service for database operations + */ + private void processAttachmentVirusScanStatus( + CmisDocument attachment, + SDMCredentials sdmCredentials, + CdsEntity attachmentDraftEntity, + PersistenceService persistenceService) { + try { + String objectId = attachment.getObjectId(); + if (objectId != null && !objectId.isEmpty()) { + logger.info( + "Processing attachment with objectId: {} and filename: {}", + objectId, + attachment.getFileName()); + + // Call getObject to check the current state + JSONObject objectResponse = sdmService.getObject(objectId, sdmCredentials, false); + + if (objectResponse != null) { + JSONObject succinctProperties = objectResponse.getJSONObject("succinctProperties"); + String currentFileName = succinctProperties.getString("cmis:name"); + + // Extract scanStatus if available + String scanStatus = null; + if (succinctProperties.has("sap:virusScanStatus")) { + scanStatus = succinctProperties.getString("sap:virusScanStatus"); + } + + logger.info( + "Successfully retrieved object for attachmentId: {}, filename: {}, scanStatus: {}", + attachment.getAttachmentId(), + currentFileName, + scanStatus); + + // Update the uploadStatus based on the scan status + if (scanStatus != null) { + SDMConstants.ScanStatus scanStatusEnum = SDMConstants.ScanStatus.fromValue(scanStatus); + dbQuery.updateUploadStatusByScanStatus( + attachmentDraftEntity, persistenceService, objectId, scanStatusEnum); + logger.info( + "Updated uploadStatus for objectId: {} based on scanStatus: {}", + objectId, + scanStatus); + } + } else { + logger.warn( + "Object not found for attachmentId: {}, objectId: {}", + attachment.getAttachmentId(), + objectId); + } + } + } catch (IOException e) { + logger.error( + "Error processing attachment with objectId: {}, error: {}", + attachment.getObjectId(), + e.getMessage()); + } catch (Exception e) { + logger.error( + "Unexpected error processing attachment with objectId: {}, error: {}", + attachment.getObjectId(), + e.getMessage()); + } + } } 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 552416c66..97a385063 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 @@ -1,5 +1,7 @@ package com.sap.cds.sdm.handler.applicationservice; +import static com.sap.cds.sdm.constants.SDMConstants.SDM_READONLY_CONTEXT; + import com.sap.cds.CdsData; import com.sap.cds.reflect.CdsEntity; import com.sap.cds.sdm.caching.CacheConfig; @@ -17,13 +19,16 @@ import com.sap.cds.services.cds.ApplicationService; import com.sap.cds.services.cds.CdsUpdateEventContext; import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.After; 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 com.sap.cds.services.utils.OrderConstants; import java.io.IOException; import java.util.*; import org.ehcache.Cache; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,7 +52,63 @@ public SDMUpdateAttachmentsHandler( } @Before - @HandlerOrder(HandlerOrder.EARLY) + @HandlerOrder(OrderConstants.Before.CHECK_CAPABILITIES - 500) + public void preserveUploadStatus(CdsUpdateEventContext context, List data) { + // Preserve uploadStatus before CDS removes readonly fields + SDMUtils.preserveReadonlyFields(context.getTarget(), data); + } + + @After + @HandlerOrder(HandlerOrder.LATE) + public void processAfter(CdsUpdateEventContext context, List data) { + // Update uploadStatus to Success after entity is persisted + logger.info( + "Post-processing attachments after persistence for entity: {}", + context.getTarget().getQualifiedName()); + + for (CdsData entityData : data) { + Map> attachmentCompositionDetails = + AttachmentsHandlerUtils.getAttachmentCompositionDetails( + context.getModel(), + context.getTarget(), + persistenceService, + context.getTarget().getQualifiedName(), + entityData); + + for (Map.Entry> entry : attachmentCompositionDetails.entrySet()) { + String attachmentCompositionDefinition = entry.getKey(); + String attachmentCompositionName = entry.getValue().get("name"); + Optional attachmentEntity = + context.getModel().findEntity(attachmentCompositionDefinition); + + if (attachmentEntity.isPresent()) { + String targetEntity = context.getTarget().getQualifiedName(); + List> attachments = + AttachmentsHandlerUtils.fetchAttachments( + targetEntity, entityData, attachmentCompositionName); + + if (attachments != null) { + for (Map attachment : attachments) { + String id = (String) attachment.get("ID"); + String uploadStatus = (String) attachment.get("uploadStatus"); + if (id != null) { + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setAttachmentId(id); + cmisDocument.setUploadStatus(uploadStatus); + // Update uploadStatus to Success in database if it was InProgress + dbQuery.saveUploadStatusToAttachment( + attachmentEntity.get(), persistenceService, cmisDocument); + logger.debug("Updated uploadStatus to Success for attachment ID: {}", id); + } + } + } + } + } + } + } + + @Before + @HandlerOrder(HandlerOrder.DEFAULT) public void processBefore(CdsUpdateEventContext context, List data) throws IOException { // Get comprehensive attachment composition details for each entity for (CdsData entityData : data) { @@ -62,6 +123,7 @@ public void processBefore(CdsUpdateEventContext context, List data) thr updateName(context, data, attachmentCompositionDetails); } + SDMUtils.cleanupReadonlyContexts(data); } public void updateName( @@ -175,6 +237,11 @@ private void processAttachments( Map secondaryPropertiesWithInvalidDefinitions, List noSDMRoles) throws IOException { + List virusDetectedFiles = new ArrayList<>(); + List virusScanInProgressFiles = new ArrayList<>(); + List scanFailedFiles = new ArrayList<>(); + List uploadInProgressFiles = new ArrayList<>(); + Iterator> iterator = attachments.iterator(); while (iterator.hasNext()) { Map attachment = iterator.next(); @@ -188,7 +255,42 @@ private void processAttachments( filesWithUnsupportedProperties, badRequest, secondaryPropertiesWithInvalidDefinitions, - noSDMRoles); + noSDMRoles, + virusDetectedFiles, + virusScanInProgressFiles, + scanFailedFiles, + uploadInProgressFiles); + } + + // Throw exception if any files failed virus scan or scan failed + if (!virusDetectedFiles.isEmpty() + || !virusScanInProgressFiles.isEmpty() + || !scanFailedFiles.isEmpty() + || !uploadInProgressFiles.isEmpty()) { + StringBuilder errorMessage = new StringBuilder(); + if (!virusDetectedFiles.isEmpty()) { + errorMessage.append(SDMErrorMessages.virusDetectedFilesMessage(virusDetectedFiles)); + } + if (!virusScanInProgressFiles.isEmpty()) { + if (errorMessage.length() > 0) { + errorMessage.append(" "); + } + errorMessage.append( + SDMErrorMessages.virusScanInProgressFilesMessage(virusScanInProgressFiles)); + } + if (!scanFailedFiles.isEmpty()) { + if (errorMessage.length() > 0) { + errorMessage.append(" "); + } + errorMessage.append(SDMErrorMessages.scanFailedFilesMessage(scanFailedFiles)); + } + if (!uploadInProgressFiles.isEmpty()) { + if (errorMessage.length() > 0) { + errorMessage.append(" "); + } + errorMessage.append(SDMErrorMessages.uploadInProgressFilesMessage(uploadInProgressFiles)); + } + throw new ServiceException(errorMessage.toString()); } SecondaryPropertiesKey secondaryPropertiesKey = new SecondaryPropertiesKey(); secondaryPropertiesKey.setRepositoryId(SDMConstants.REPOSITORY_ID); @@ -208,7 +310,11 @@ public void processAttachment( List filesWithUnsupportedProperties, Map badRequest, Map secondaryPropertiesWithInvalidDefinitions, - List noSDMRoles) + List noSDMRoles, + List virusDetectedFiles, + List virusScanInProgressFiles, + List scanFailedFiles, + List uploadInProgressFiles) throws IOException { String id = (String) attachment.get("ID"); String filenameInRequest = (String) attachment.get("fileName"); @@ -219,33 +325,145 @@ public void processAttachment( Map secondaryTypeProperties = SDMUtils.getSecondaryTypeProperties(attachmentEntity, attachment); - String fileNameInDB = + CmisDocument cmisDocument = dbQuery.getAttachmentForID(attachmentEntity.get(), persistenceService, id); + String fileNameInDB = cmisDocument.getFileName(); + + // Check for upload status issues + if (handleUploadStatusCheck( + attachment, + fileNameInDB, + filenameInRequest, + virusDetectedFiles, + virusScanInProgressFiles, + scanFailedFiles, + uploadInProgressFiles)) { + return; + } + + // Fetch file details from SDM if needed SDMCredentials sdmCredentials = tokenHandler.getSDMCredentials(); + AttachmentDetails details = + fetchAttachmentDetails( + fileNameInDB, + descriptionInRequest, + objectId, + sdmCredentials, + context.getUserInfo().isSystemUser()); + + Map propertiesInDB = + dbQuery.getPropertiesForID( + attachmentEntity.get(), persistenceService, id, secondaryTypeProperties); - String fileNameInSDM = null; - String descriptionInSDM = null; + Map updatedSecondaryProperties = + prepareUpdatedProperties( + attachmentEntity, + attachment, + filenameInRequest, + descriptionInRequest, + details.fileNameInDB, + details.descriptionInDB, + secondaryTypeProperties, + propertiesInDB); + + if (updatedSecondaryProperties.isEmpty()) { + logger.debug("No changes detected for attachment ID: {}, skipping SDM update", id); + return; + } + + updateAttachmentInSDM( + context, + attachment, + id, + filenameInRequest, + descriptionInRequest, + objectId, + details.fileNameInDB, + details.descriptionInDB, + propertiesInDB, + secondaryTypeProperties, + updatedSecondaryProperties, + secondaryPropertiesWithInvalidDefinitions, + noSDMRoles, + duplicateFileNameList, + filesNotFound, + filesWithUnsupportedProperties, + badRequest); + } + + private boolean handleUploadStatusCheck( + Map attachment, + String fileNameInDB, + String filenameInRequest, + List virusDetectedFiles, + List virusScanInProgressFiles, + List scanFailedFiles, + List uploadInProgressFiles) { + Map readonlyData = (Map) attachment.get(SDM_READONLY_CONTEXT); + if (readonlyData == null || readonlyData.get("uploadStatus") == null) { + return false; + } + + String uploadStatus = readonlyData.get("uploadStatus").toString(); + String fileName = fileNameInDB != null ? fileNameInDB : filenameInRequest; + + if (uploadStatus.equalsIgnoreCase(SDMConstants.UPLOAD_STATUS_VIRUS_DETECTED)) { + virusDetectedFiles.add(fileName); + return true; + } + if (uploadStatus.equalsIgnoreCase(SDMConstants.VIRUS_SCAN_INPROGRESS)) { + virusScanInProgressFiles.add(fileName); + return true; + } + if (uploadStatus.equalsIgnoreCase(SDMConstants.UPLOAD_STATUS_SCAN_FAILED)) { + scanFailedFiles.add(fileName); + return true; + } + if (uploadStatus.equalsIgnoreCase(SDMConstants.UPLOAD_STATUS_IN_PROGRESS)) { + uploadInProgressFiles.add(fileName); + return true; + } + + attachment.put("uploadStatus", uploadStatus); + return false; + } + + private AttachmentDetails fetchAttachmentDetails( + String fileNameInDB, + String descriptionInRequest, + String objectId, + SDMCredentials sdmCredentials, + boolean isSystemUser) + throws IOException { + String finalFileNameInDB = fileNameInDB; + String descriptionInDB = null; if (fileNameInDB == null || descriptionInRequest != null) { - List sdmAttachmentData = + JSONObject sdmAttachmentData = AttachmentsHandlerUtils.fetchAttachmentDataFromSDM( - sdmService, objectId, sdmCredentials, context.getUserInfo().isSystemUser()); + sdmService, objectId, sdmCredentials, isSystemUser); + JSONObject succinctProperties = sdmAttachmentData.getJSONObject("succinctProperties"); - if (sdmAttachmentData != null && sdmAttachmentData.size() >= 2) { - fileNameInSDM = sdmAttachmentData.get(0); - descriptionInSDM = sdmAttachmentData.get(1); + if (succinctProperties.has("cmis:name")) { + finalFileNameInDB = succinctProperties.getString("cmis:name"); } - - if (fileNameInDB == null) { - fileNameInDB = fileNameInSDM; + if (succinctProperties.has("cmis:description")) { + descriptionInDB = succinctProperties.getString("cmis:description"); } } - Map propertiesInDB = - dbQuery.getPropertiesForID( - attachmentEntity.get(), persistenceService, id, secondaryTypeProperties); - // Prepare document and updated properties + return new AttachmentDetails(finalFileNameInDB, descriptionInDB); + } + private Map prepareUpdatedProperties( + Optional attachmentEntity, + Map attachment, + String filenameInRequest, + String descriptionInRequest, + String fileNameInDB, + String descriptionInDB, + Map secondaryTypeProperties, + Map propertiesInDB) { Map updatedSecondaryProperties = SDMUtils.getUpdatedSecondaryProperties( attachmentEntity, @@ -254,28 +472,42 @@ public void processAttachment( secondaryTypeProperties, propertiesInDB); - CmisDocument cmisDocument = - AttachmentsHandlerUtils.prepareCmisDocument( - filenameInRequest, descriptionInRequest, objectId); - - // Update filename and description properties AttachmentsHandlerUtils.updateFilenameProperty( - fileNameInDB, filenameInRequest, fileNameInSDM, updatedSecondaryProperties); + fileNameInDB, filenameInRequest, fileNameInDB, updatedSecondaryProperties); AttachmentsHandlerUtils.updateDescriptionProperty( - null, descriptionInRequest, descriptionInSDM, updatedSecondaryProperties, true); + null, descriptionInRequest, descriptionInDB, updatedSecondaryProperties, true); - // Send update to SDM only if there are changes - if (updatedSecondaryProperties.isEmpty()) { - logger.debug("No changes detected for attachment ID: {}, skipping SDM update", id); - return; - } + return updatedSecondaryProperties; + } + private void updateAttachmentInSDM( + CdsUpdateEventContext context, + Map attachment, + String id, + String filenameInRequest, + String descriptionInRequest, + String objectId, + String fileNameInDB, + String descriptionInDB, + Map propertiesInDB, + Map secondaryTypeProperties, + Map updatedSecondaryProperties, + Map secondaryPropertiesWithInvalidDefinitions, + List noSDMRoles, + List duplicateFileNameList, + List filesNotFound, + List filesWithUnsupportedProperties, + Map badRequest) { logger.debug( "Updating attachment in SDM - ID: {}, properties count: {}", id, updatedSecondaryProperties.size()); + CmisDocument cmisDocument = + AttachmentsHandlerUtils.prepareCmisDocument( + filenameInRequest, descriptionInRequest, objectId); + try { int responseCode = sdmService.updateAttachments( @@ -294,7 +526,7 @@ public void processAttachment( filenameInRequest, propertiesInDB, secondaryTypeProperties, - descriptionInSDM, + descriptionInDB, noSDMRoles, duplicateFileNameList, filesNotFound); @@ -310,12 +542,22 @@ public void processAttachment( filenameInRequest, propertiesInDB, secondaryTypeProperties, - descriptionInSDM, + descriptionInDB, filesWithUnsupportedProperties, badRequest); } } + private static class AttachmentDetails { + final String fileNameInDB; + final String descriptionInDB; + + AttachmentDetails(String fileNameInDB, String descriptionInDB) { + this.fileNameInDB = fileNameInDB; + this.descriptionInDB = descriptionInDB; + } + } + private void handleWarnings( CdsUpdateEventContext context, List duplicateFileNameList, diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java index 2c832e7fd..5fe996334 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/AttachmentsHandlerUtils.java @@ -16,6 +16,7 @@ import com.sap.cds.services.persistence.PersistenceService; import java.io.IOException; import java.util.*; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -613,7 +614,7 @@ public static Boolean validateFileNames( * @return a list containing [filename, description] * @throws IOException if there's an error fetching from SDM */ - public static List fetchAttachmentDataFromSDM( + public static JSONObject fetchAttachmentDataFromSDM( SDMService sdmService, String objectId, SDMCredentials sdmCredentials, boolean isSystemUser) throws IOException { return sdmService.getObject(objectId, sdmCredentials, isSystemUser); diff --git a/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/SDMBeforeReadItemsModifier.java b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/SDMBeforeReadItemsModifier.java new file mode 100644 index 000000000..6a1b64832 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/helper/SDMBeforeReadItemsModifier.java @@ -0,0 +1,101 @@ +package com.sap.cds.sdm.handler.applicationservice.helper; + +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.Attachments; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; +import com.sap.cds.ql.CQL; +import com.sap.cds.ql.Expand; +import com.sap.cds.ql.cqn.CqnSelectListItem; +import com.sap.cds.ql.cqn.Modifier; +import java.util.ArrayList; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The class {@link SDMBeforeReadItemsModifier} is a modifier that adds the repository id filter and + * ensures proper handling of expanded associations like statusNav/uploadStatusNav. + */ +public class SDMBeforeReadItemsModifier implements Modifier { + + private static final Logger logger = LoggerFactory.getLogger(SDMBeforeReadItemsModifier.class); + + private static final String ROOT_ASSOCIATION = ""; + private final List mediaAssociations; + + public SDMBeforeReadItemsModifier(List mediaAssociations) { + this.mediaAssociations = mediaAssociations; + } + + @Override + public List items(List items) { + List newItems = + new ArrayList<>(items.stream().filter(item -> !item.isExpand()).toList()); + List result = addRequiredFields(items); + newItems.addAll(result); + + return newItems; + } + + private List addRequiredFields(List list) { + List newItems = new ArrayList<>(); + enhanceWithRequiredFieldsForMediaAssociation(ROOT_ASSOCIATION, list, newItems); + + List expandedItems = + list.stream().filter(CqnSelectListItem::isExpand).toList(); + newItems.addAll(processExpandedEntities(expandedItems)); + return newItems; + } + + private List processExpandedEntities(List expandedItems) { + List newItems = new ArrayList<>(); + + expandedItems.forEach( + item -> { + List newItemsFromExpand = + new ArrayList<>(item.asExpand().items().stream().filter(i -> !i.isExpand()).toList()); + enhanceWithRequiredFieldsForMediaAssociation( + item.asExpand().displayName(), newItemsFromExpand, newItemsFromExpand); + List expandedSubItems = + item.asExpand().items().stream().filter(CqnSelectListItem::isExpand).toList(); + List result = processExpandedEntities(expandedSubItems); + newItemsFromExpand.addAll(result); + Expand copy = CQL.copy(item.asExpand()); + copy.items(newItemsFromExpand); + newItems.add(copy); + }); + + return newItems; + } + + private void enhanceWithRequiredFieldsForMediaAssociation( + String association, List list, List listToEnhance) { + if (isMediaAssociationAndNeedRequiredFields(association, list)) { + logger.debug( + "Adding required fields (contentId, status, repositoryId, uploadStatus) to select items"); + if (list.stream().noneMatch(item -> isItemRefFieldWithName(item, Attachments.CONTENT_ID))) { + listToEnhance.add(CQL.get(Attachments.CONTENT_ID)); + } + if (list.stream().noneMatch(item -> isItemRefFieldWithName(item, Attachments.STATUS))) { + listToEnhance.add(CQL.get(Attachments.STATUS)); + } + if (list.stream().noneMatch(item -> isItemRefFieldWithName(item, "repositoryId"))) { + listToEnhance.add(CQL.get("repositoryId")); + } + if (list.stream().noneMatch(item -> isItemRefFieldWithName(item, "uploadStatus"))) { + listToEnhance.add(CQL.get("uploadStatus")); + } + } + } + + private boolean isMediaAssociationAndNeedRequiredFields( + String association, List list) { + // Only add fields for actual media associations, not the root entity (empty string) + return !association.equals(ROOT_ASSOCIATION) + && mediaAssociations.contains(association) + && list.stream().anyMatch(item -> isItemRefFieldWithName(item, MediaData.CONTENT)); + } + + private boolean isItemRefFieldWithName(CqnSelectListItem item, String fieldName) { + return item.isRef() && item.asRef().displayName().equals(fieldName); + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/model/CmisDocument.java b/sdm/src/main/java/com/sap/cds/sdm/model/CmisDocument.java index 92f2276d5..ae3ccd46a 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/model/CmisDocument.java +++ b/sdm/src/main/java/com/sap/cds/sdm/model/CmisDocument.java @@ -29,6 +29,7 @@ public class CmisDocument { private String url; private String contentId; private String type; + private String uploadStatus; private String description; private Map secondaryProperties; } diff --git a/sdm/src/main/java/com/sap/cds/sdm/model/RepoValue.java b/sdm/src/main/java/com/sap/cds/sdm/model/RepoValue.java index 005df0820..4a2e58f72 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/model/RepoValue.java +++ b/sdm/src/main/java/com/sap/cds/sdm/model/RepoValue.java @@ -11,4 +11,5 @@ public class RepoValue { private Boolean virusScanEnabled; private Boolean versionEnabled; private Boolean disableVirusScannerForLargeFile; + private Boolean isAsyncVirusScanEnabled; } diff --git a/sdm/src/main/java/com/sap/cds/sdm/model/Repository.java b/sdm/src/main/java/com/sap/cds/sdm/model/Repository.java index c41be0dd6..7c3c72a47 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/model/Repository.java +++ b/sdm/src/main/java/com/sap/cds/sdm/model/Repository.java @@ -25,6 +25,7 @@ public class Repository { private Boolean isEncryptionEnabled; private Boolean isThumbnailEnabled; private Boolean isContentBridgeEnabled; + private Boolean isAsyncVirusScanEnabled; private String hashAlgorithms; private List repositoryParams; } 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 8eac460bf..4966f2fb7 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 @@ -3,6 +3,7 @@ import com.sap.cds.Result; import com.sap.cds.Row; import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; import com.sap.cds.ql.Delete; import com.sap.cds.ql.Select; import com.sap.cds.ql.Update; @@ -56,7 +57,14 @@ public CmisDocument getObjectIdForAttachmentID( CdsEntity attachmentEntity, PersistenceService persistenceService, String id) { CqnSelect q = Select.from(attachmentEntity) - .columns("objectId", "folderId", "fileName", "mimeType", "contentId", "linkUrl") + .columns( + "objectId", + "folderId", + "fileName", + "mimeType", + "contentId", + "linkUrl", + "uploadStatus") .where(doc -> doc.get("ID").eq(id)); Result result = persistenceService.run(q); Optional res = result.first(); @@ -70,6 +78,8 @@ public CmisDocument getObjectIdForAttachmentID( cmisDocument.setContentId( row.get("contentId") != null ? row.get("contentId").toString() : null); cmisDocument.setUrl(row.get("linkUrl") != null ? row.get("linkUrl").toString() : null); + cmisDocument.setUploadStatus( + row.get("uploadStatus") != null ? row.get("uploadStatus").toString() : null); } return cmisDocument; } @@ -328,12 +338,16 @@ public Result getAttachmentsForUPIDAndRepository( return persistenceService.run(q); } - public String getAttachmentForID( + public CmisDocument getAttachmentForID( CdsEntity attachmentEntity, PersistenceService persistenceService, String id) { CqnSelect q = Select.from(attachmentEntity).columns("fileName").where(doc -> doc.get("ID").eq(id)); Result result = persistenceService.run(q); - return result.rowCount() == 0 ? null : result.list().get(0).get("fileName").toString(); + CmisDocument cmisDocument = new CmisDocument(); + for (Row row : result.list()) { + cmisDocument.setFileName(row.get("fileName").toString()); + } + return cmisDocument; } public void addAttachmentToDraft( @@ -348,6 +362,20 @@ public void addAttachmentToDraft( updatedFields.put("status", "Clean"); updatedFields.put("type", "sap-icon://document"); updatedFields.put("mimeType", cmisDocument.getMimeType()); + updatedFields.put("uploadStatus", cmisDocument.getUploadStatus()); + CqnUpdate updateQuery = + Update.entity(attachmentEntity) + .data(updatedFields) + .where(doc -> doc.get("ID").eq(cmisDocument.getAttachmentId())); + persistenceService.run(updateQuery); + } + + public void saveUploadStatusToAttachment( + CdsEntity attachmentEntity, + PersistenceService persistenceService, + CmisDocument cmisDocument) { + Map updatedFields = new HashMap<>(); + updatedFields.put("uploadStatus", cmisDocument.getUploadStatus()); CqnUpdate updateQuery = Update.entity(attachmentEntity) .data(updatedFields) @@ -364,7 +392,14 @@ public List getAttachmentsForFolder( List cmisDocuments = new ArrayList<>(); CqnSelect q = Select.from(attachmentEntity.get()) - .columns("fileName", "IsActiveEntity", "ID", "folderId", "repositoryId", "objectId") + .columns( + "fileName", + "IsActiveEntity", + "ID", + "folderId", + "repositoryId", + "objectId", + "uploadStatus") .where(doc -> doc.get("folderId").eq(folderId)); Result result = persistenceService.run(q); for (Row row : result.list()) { @@ -374,13 +409,24 @@ public List getAttachmentsForFolder( cmisDocument.setFileName(row.get("fileName").toString()); cmisDocument.setAttachmentId(row.get("ID").toString()); cmisDocument.setObjectId(row.get("objectId").toString()); + cmisDocument.setUploadStatus( + row.get("uploadStatus") != null + ? row.get("uploadStatus").toString() + : SDMConstants.UPLOAD_STATUS_IN_PROGRESS); cmisDocuments.add(cmisDocument); } if (cmisDocuments.isEmpty()) { attachmentEntity = context.getModel().findEntity(entity); q = Select.from(attachmentEntity.get()) - .columns("fileName", "IsActiveEntity", "ID", "folderId", "repositoryId", "objectId") + .columns( + "fileName", + "IsActiveEntity", + "ID", + "folderId", + "repositoryId", + "objectId", + "uploadStatus") .where(doc -> doc.get("folderId").eq(folderId)); result = persistenceService.run(q); for (Row row : result.list()) { @@ -390,6 +436,10 @@ public List getAttachmentsForFolder( cmisDocument.setFileName(row.get("fileName").toString()); cmisDocument.setAttachmentId(row.get("ID").toString()); cmisDocument.setObjectId(row.get("objectId").toString()); + cmisDocument.setUploadStatus( + row.get("uploadStatus") != null + ? row.get("uploadStatus").toString() + : SDMConstants.UPLOAD_STATUS_IN_PROGRESS); cmisDocuments.add(cmisDocument); } } @@ -400,41 +450,176 @@ public Map getPropertiesForID( CdsEntity attachmentEntity, PersistenceService persistenceService, String id, - List properties) { + Map properties) { CqnSelect q = Select.from(attachmentEntity) - .columns(properties.toArray(new String[0])) + .columns(properties.keySet().toArray(new String[0])) .where(doc -> doc.get("ID").eq(id)); Result result = persistenceService.run(q); Map propertyValueMap = new HashMap<>(); - for (String property : properties) { + for (Map.Entry entry : properties.entrySet()) { + String property = entry.getKey(); + String mapKey = entry.getValue(); Object value = result.rowCount() > 0 ? result.list().get(0).get(property) : null; - propertyValueMap.put(property, value != null ? value.toString() : null); + propertyValueMap.put(mapKey, value != null ? value.toString() : null); } - return propertyValueMap; } - public Map getPropertiesForID( + public CmisDocument getuploadStatusForAttachment( + String entity, + PersistenceService persistenceService, + String objectId, + AttachmentReadEventContext context) { + Optional attachmentEntity = context.getModel().findEntity(entity + "_drafts"); + CqnSelect q = + Select.from(attachmentEntity.get()) + .columns("uploadStatus") + .where(doc -> doc.get("objectId").eq(objectId)); + Result result = persistenceService.run(q); + CmisDocument cmisDocument = new CmisDocument(); + boolean isAttachmentFound = false; + for (Row row : result.list()) { + cmisDocument.setUploadStatus( + row.get("uploadStatus") != null + ? row.get("uploadStatus").toString() + : SDMConstants.UPLOAD_STATUS_IN_PROGRESS); + isAttachmentFound = true; + } + if (!isAttachmentFound) { + attachmentEntity = context.getModel().findEntity(entity); + q = + Select.from(attachmentEntity.get()) + .columns("uploadStatus") + .where(doc -> doc.get("objectId").eq(objectId)); + result = persistenceService.run(q); + for (Row row : result.list()) { + cmisDocument.setUploadStatus( + row.get("uploadStatus") != null + ? row.get("uploadStatus").toString() + : SDMConstants.UPLOAD_STATUS_IN_PROGRESS); + } + } + return cmisDocument; + } + + public List getAttachmentsWithVirusScanInProgress( CdsEntity attachmentEntity, PersistenceService persistenceService, - String id, - Map properties) { + String upID, + String upIDkey) { CqnSelect q = Select.from(attachmentEntity) - .columns(properties.keySet().toArray(new String[0])) - .where(doc -> doc.get("ID").eq(id)); + .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 result = persistenceService.run(q); - Map propertyValueMap = new HashMap<>(); - for (Map.Entry entry : properties.entrySet()) { - String property = entry.getKey(); - String mapKey = entry.getValue(); - Object value = result.rowCount() > 0 ? result.list().get(0).get(property) : null; - propertyValueMap.put(mapKey, value != null ? value.toString() : null); + List attachments = new ArrayList<>(); + for (Row row : result.list()) { + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setAttachmentId(row.get("ID") != null ? row.get("ID").toString() : null); + cmisDocument.setObjectId(row.get("objectId") != null ? row.get("objectId").toString() : null); + cmisDocument.setFileName(row.get("fileName") != null ? row.get("fileName").toString() : null); + cmisDocument.setFolderId(row.get("folderId") != null ? row.get("folderId").toString() : null); + cmisDocument.setRepositoryId( + row.get("repositoryId") != null ? row.get("repositoryId").toString() : null); + cmisDocument.setMimeType(row.get("mimeType") != null ? row.get("mimeType").toString() : null); + cmisDocument.setUploadStatus( + row.get("uploadStatus") != null + ? row.get("uploadStatus").toString() + : SDMConstants.UPLOAD_STATUS_IN_PROGRESS); + attachments.add(cmisDocument); + } + return attachments; + } + + /** + * Updates uploadStatus to 'SUCCESS' for all attachments where uploadStatus is + * UPLOAD_STATUS_IN_PROGRESS for a given up__ID. + * + * @param attachmentEntity the attachment entity + * @param persistenceService the persistence service + * @param upID the up__ID to filter attachments + * @param upIdKey the key name for up__ID field (e.g., "up__ID") + */ + public void updateInProgressUploadStatusToSuccess( + CdsEntity attachmentEntity, + PersistenceService persistenceService, + String upID, + String upIdKey) { + CqnSelect q = + Select.from(attachmentEntity) + .columns("objectId", "uploadStatus") + .where(doc -> doc.get(upIdKey).eq(upID)); + Result selectRes = persistenceService.run(q); + for (Row row : selectRes.list()) { + if (row.get("uploadStatus") == null + || row.get("uploadStatus") + .toString() + .equalsIgnoreCase(SDMConstants.UPLOAD_STATUS_IN_PROGRESS) + && row.get("objectId") != null) { + CqnUpdate updateQuery = + Update.entity(attachmentEntity) + .data("uploadStatus", SDMConstants.UPLOAD_STATUS_SUCCESS) + .where( + doc -> + doc.get(upIdKey) + .eq(upID) + .and( + doc.get("uploadStatus") + .isNull() + .or( + doc.get("uploadStatus") + .eq(SDMConstants.UPLOAD_STATUS_IN_PROGRESS)))); + + persistenceService.run(updateQuery); + } + } + } + + public Result updateUploadStatusByScanStatus( + CdsEntity attachmentEntity, + 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)); + + return persistenceService.run(updateQuery); + } + + private String mapScanStatusToUploadStatus(SDMConstants.ScanStatus scanStatus) { + switch (scanStatus) { + case QUARANTINED: + return SDMConstants.UPLOAD_STATUS_VIRUS_DETECTED; + case PENDING: + return SDMConstants.UPLOAD_STATUS_IN_PROGRESS; + case SCANNING: + return SDMConstants.VIRUS_SCAN_INPROGRESS; + case FAILED: + return SDMConstants.UPLOAD_STATUS_SCAN_FAILED; + case CLEAN: + return SDMConstants.UPLOAD_STATUS_SUCCESS; + case BLANK: + default: + return SDMConstants.UPLOAD_STATUS_SUCCESS; } - return propertyValueMap; } /** diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/DocumentUploadService.java b/sdm/src/main/java/com/sap/cds/sdm/service/DocumentUploadService.java index ebab986da..dc80e930a 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/DocumentUploadService.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/DocumentUploadService.java @@ -3,6 +3,9 @@ import static com.sap.cds.sdm.constants.SDMConstants.NAMED_USER_FLOW; import static com.sap.cds.sdm.constants.SDMConstants.TECHNICAL_USER_FLOW; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; import com.sap.cds.sdm.constants.SDMConstants; import com.sap.cds.sdm.constants.SDMErrorMessages; import com.sap.cds.sdm.handler.TokenHandler; @@ -52,7 +55,10 @@ public DocumentUploadService( * Implementation to create document. */ public JSONObject createDocument( - CmisDocument cmisDocument, SDMCredentials sdmCredentials, boolean isSystemUser) + CmisDocument cmisDocument, + SDMCredentials sdmCredentials, + boolean isSystemUser, + AttachmentCreateEventContext eventContext) throws IOException { try { if ("application/internet-shortcut".equalsIgnoreCase(cmisDocument.getMimeType())) { @@ -61,8 +67,12 @@ public JSONObject createDocument( } long totalSize = cmisDocument.getContentLength(); int chunkSize = SDMConstants.CHUNK_SIZE; - + CdsModel model = eventContext.getModel(); + Optional attachmentDraftEntity = + model.findEntity(eventContext.getAttachmentEntity() + "_drafts"); + cmisDocument.setUploadStatus(SDMConstants.UPLOAD_STATUS_IN_PROGRESS); if (totalSize <= 400 * 1024 * 1024) { + // Upload directly if file is ≤ 400MB return uploadSingleChunk(cmisDocument, sdmCredentials, isSystemUser); } else { @@ -92,7 +102,7 @@ private void executeHttpPost( /* * CMIS call to appending content stream */ - private void appendContentStream( + private JSONObject appendContentStream( CmisDocument cmisDocument, String sdmUrl, byte[] chunkBuffer, @@ -131,7 +141,7 @@ private void appendContentStream( try { this.executeHttpPost(httpClient, request, cmisDocument, finalResponse); cmisDocument.setMimeType(finalResponse.get("mimeType")); - + return new JSONObject(finalResponse); } catch (Exception e) { logger.error("Error in appending content: {}", e.getMessage()); throw new IOException("Error in appending content: " + e.getMessage(), e); @@ -227,7 +237,6 @@ private JSONObject uploadLargeFileInChunks( // set in every chunk appendContent JSONObject responseBody = createEmptyDocument(cmisDocument, sdmUrl, isSystemUser); logger.info("Response Body: {}", responseBody); - String objectId = responseBody.getString("objectId"); cmisDocument.setObjectId(objectId); logger.info("objectId of empty doc is {}", objectId); @@ -269,8 +278,15 @@ private JSONObject uploadLargeFileInChunks( // Step 7: Append Chunk. Call cmis api to append content stream if (bytesRead > 0) { - appendContentStream( - cmisDocument, sdmUrl, chunkBuffer, bytesRead, isLastChunk, chunkIndex, isSystemUser); + responseBody = + appendContentStream( + cmisDocument, + sdmUrl, + chunkBuffer, + bytesRead, + isLastChunk, + chunkIndex, + isSystemUser); } long endChunkUploadTime = System.currentTimeMillis(); @@ -301,7 +317,7 @@ private void formResponse( String status = "success"; String name = cmisDocument.getFileName(); String id = cmisDocument.getAttachmentId(); - String objectId = "", mimeType = ""; + String objectId = "", mimeType = "", scanStatus = ""; String error = ""; try { String responseString = EntityUtils.toString(response.getEntity()); @@ -311,6 +327,10 @@ private void formResponse( JSONObject succinctProperties = jsonResponse.getJSONObject("succinctProperties"); status = "success"; objectId = succinctProperties.getString("cmis:objectId"); + scanStatus = + succinctProperties.has("sap:virusScanStatus") + ? succinctProperties.getString("sap:virusScanStatus") + : null; mimeType = succinctProperties.has("cmis:contentStreamMimeType") ? succinctProperties.getString("cmis:contentStreamMimeType") @@ -347,6 +367,32 @@ private void formResponse( if (!objectId.isEmpty()) { finalResponse.put("objectId", objectId); finalResponse.put("mimeType", mimeType); + + // Determine upload status based on scan status using enum + SDMConstants.ScanStatus scanStatusEnum = SDMConstants.ScanStatus.fromValue(scanStatus); + String uploadStatus; + switch (scanStatusEnum) { + case QUARANTINED: + uploadStatus = SDMConstants.UPLOAD_STATUS_VIRUS_DETECTED; + break; + case SCANNING: + uploadStatus = SDMConstants.VIRUS_SCAN_INPROGRESS; + break; + case FAILED: + uploadStatus = SDMConstants.UPLOAD_STATUS_SCAN_FAILED; + break; + case CLEAN: + uploadStatus = SDMConstants.UPLOAD_STATUS_SUCCESS; + break; + case PENDING: + uploadStatus = SDMConstants.UPLOAD_STATUS_IN_PROGRESS; + break; + case BLANK: + default: + uploadStatus = SDMConstants.UPLOAD_STATUS_SUCCESS; + break; + } + finalResponse.put("uploadStatus", uploadStatus); } } catch (IOException e) { throw new ServiceException( 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 9c826ebc5..440fdfb7a 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 @@ -47,8 +47,8 @@ public int updateAttachments( boolean isSystemUser) throws ServiceException; - public List getObject( - String objectId, SDMCredentials sdmCredentials, boolean isSystemUser) throws IOException; + public JSONObject getObject(String objectId, SDMCredentials sdmCredentials, boolean isSystemUser) + throws IOException; public List getSecondaryTypes( String repositoryId, SDMCredentials sdmCredentials, boolean isSystemUser) 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 04a84010b..7763bdc95 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 @@ -147,7 +147,6 @@ private void formResponse( try { String responseString = EntityUtils.toString(response.getEntity()); int responseCode = response.getStatusLine().getStatusCode(); - if (responseCode == 201 || responseCode == 200) { status = "success"; JSONObject jsonResponse = new JSONObject(responseString); @@ -255,6 +254,7 @@ public int updateAttachments( Collectors .toSet()); // Adding the properties which are unsupported to a list so that // exeception can be thrown + Set keysMap1 = secondaryProperties.keySet(); for (Map.Entry entry : secondaryPropertiesWithInvalidDefinitions @@ -326,8 +326,8 @@ public int updateAttachments( } @Override - public List getObject( - String objectId, SDMCredentials sdmCredentials, boolean isSystemUser) throws IOException { + public JSONObject getObject(String objectId, SDMCredentials sdmCredentials, boolean isSystemUser) + throws IOException { String grantType = isSystemUser ? TECHNICAL_USER_FLOW : NAMED_USER_FLOW; logger.info("This is a :" + grantType + " flow"); var httpClient = tokenHandler.getHttpClient(binding, connectionPool, null, grantType); @@ -343,14 +343,13 @@ public List getObject( HttpGet getObjectRequest = new HttpGet(sdmUrl); try (var response = (CloseableHttpResponse) httpClient.execute(getObjectRequest)) { if (response.getStatusLine().getStatusCode() != 200) { - return Collections.emptyList(); + if (response.getStatusLine().getStatusCode() == 403) { + throw new ServiceException(SDMConstants.USER_NOT_AUTHORISED_ERROR); + } + return null; } String responseString = EntityUtils.toString(response.getEntity()); - JSONObject jsonObject = new JSONObject(responseString); - JSONObject succinctProperties = jsonObject.getJSONObject("succinctProperties"); - String cmisName = succinctProperties.optString("cmis:name", ""); - String cmisDescription = succinctProperties.optString("cmis:description", ""); - return List.of(cmisName, cmisDescription); + return new JSONObject(responseString); } catch (IOException e) { throw new ServiceException(SDMUtils.getErrorMessage("ATTACHMENT_NOT_FOUND"), e); } @@ -604,6 +603,7 @@ public Map fetchRepositoryData(JSONObject repoInfo, String re // Fetch the disableVirusScannerForLargeFile repoValue.setDisableVirusScannerForLargeFile( featureData.getBoolean("disableVirusScannerForLargeFile")); + repoValue.setIsAsyncVirusScanEnabled(featureData.getBoolean("isAsyncVirusScanEnabled")); } } repoValueMap.put(repositoryId, repoValue); diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java index 3ff7485bb..2f710096a 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java @@ -20,8 +20,7 @@ import com.sap.cds.sdm.utilities.SDMUtils; import com.sap.cds.services.ServiceException; import com.sap.cds.services.handler.EventHandler; -import com.sap.cds.services.handler.annotations.On; -import com.sap.cds.services.handler.annotations.ServiceName; +import com.sap.cds.services.handler.annotations.*; import com.sap.cds.services.persistence.PersistenceService; import com.sap.cds.services.utils.StringUtils; import java.io.IOException; @@ -32,7 +31,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@ServiceName(value = "*", type = AttachmentService.class) +@ServiceName( + value = "*", + type = {AttachmentService.class}) public class SDMAttachmentsServiceHandler implements EventHandler { private final PersistenceService persistenceService; private final SDMService sdmService; @@ -98,7 +99,21 @@ public void restoreAttachment(AttachmentRestoreEventContext context) { public void readAttachment(AttachmentReadEventContext context) throws IOException { String[] contentIdParts = context.getContentId().split(":"); String objectId = contentIdParts[0]; + String entity = contentIdParts.length > 2 ? contentIdParts[2] : contentIdParts[0]; SDMCredentials sdmCredentials = tokenHandler.getSDMCredentials(); + CmisDocument cmisDocument = + dbQuery.getuploadStatusForAttachment(entity, persistenceService, objectId, context); + if (cmisDocument.getUploadStatus() != null + && cmisDocument + .getUploadStatus() + .equalsIgnoreCase(SDMConstants.UPLOAD_STATUS_VIRUS_DETECTED)) + throw new ServiceException(SDMUtils.getErrorMessage("VIRUS_DETECTED_FILE_ERROR")); + if (cmisDocument.getUploadStatus() != null + && cmisDocument.getUploadStatus().equalsIgnoreCase(SDMConstants.VIRUS_SCAN_INPROGRESS)) + throw new ServiceException(SDMUtils.getErrorMessage("VIRUS_SCAN_IN_PROGRESS_FILE_ERROR")); + if (cmisDocument.getUploadStatus() != null + && cmisDocument.getUploadStatus().equalsIgnoreCase(SDMConstants.UPLOAD_STATUS_IN_PROGRESS)) + throw new ServiceException(SDMUtils.getErrorMessage("UPLOAD_IN_PROGRESS_FILE_ERROR")); try { sdmService.readDocument(objectId, sdmCredentials, context); } catch (Exception e) { @@ -151,7 +166,8 @@ private void validateRepository(AttachmentCreateEventContext eventContext) String len = eventContext.getParameterInfo().getHeaders().get("content-length"); long contentLen = !StringUtils.isEmpty(len) ? Long.parseLong(len) : -1; // Check if repository is virus scanned - if (repoValue.getVirusScanEnabled() + if (!repoValue.getIsAsyncVirusScanEnabled() + && repoValue.getVirusScanEnabled() && contentLen > 400 * 1024 * 1024 && !repoValue.getDisableVirusScannerForLargeFile()) { throw new ServiceException(SDMUtils.getErrorMessage("VIRUS_REPO_ERROR_MORE_THAN_400MB")); @@ -241,7 +257,8 @@ private void createDocumentInSDM( SDMCredentials sdmCredentials = tokenHandler.getSDMCredentials(); JSONObject createResult = null; try { - createResult = documentService.createDocument(cmisDocument, sdmCredentials, isSystemUser); + createResult = + documentService.createDocument(cmisDocument, sdmCredentials, isSystemUser, eventContext); logger.info("Synchronous Response from documentService: {}", createResult); logger.info("Upload Finished at: {}", System.currentTimeMillis()); } catch (Exception e) { @@ -298,6 +315,10 @@ private void handleCreateDocumentResult( throw new ServiceException(SDMUtils.getErrorMessage("MIMETYPE_INVALID_ERROR")); default: cmisDocument.setObjectId(createResult.get("objectId").toString()); + cmisDocument.setUploadStatus( + (createResult.get("uploadStatus") != null) + ? createResult.get("uploadStatus").toString() + : SDMConstants.UPLOAD_STATUS_IN_PROGRESS); dbQuery.addAttachmentToDraft( getAttachmentDraftEntity(eventContext), persistenceService, cmisDocument); finalizeContext(eventContext, cmisDocument); @@ -311,7 +332,7 @@ private void finalizeContext( + ":" + cmisDocument.getFolderId() + ":" - + eventContext.getAttachmentEntity()); + + eventContext.getAttachmentEntity().getQualifiedName()); eventContext.getData().setStatus("Clean"); eventContext.getData().setContent(null); eventContext.setCompleted(); diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMCustomServiceHandler.java b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMCustomServiceHandler.java index 453007c5b..d13556614 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMCustomServiceHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMCustomServiceHandler.java @@ -554,6 +554,7 @@ private CopyAttachmentsResult copyAttachmentsToSDM( CmisDocument populatedDocument = new CmisDocument(); populatedDocument.setType(cmisDocument.getType()); populatedDocument.setUrl(cmisDocument.getUrl()); + populatedDocument.setUploadStatus(cmisDocument.getUploadStatus()); populatedDocuments.add(populatedDocument); try { @@ -964,7 +965,7 @@ private CmisDocument fetchAttachmentMetadata( */ private CmisDocument fetchAttachmentMetadataFromSDM(AttachmentMoveContext moveContext) { try { - List sdmMetadata = + JSONObject sdmMetadata = sdmService.getObject( moveContext.getObjectId(), moveContext.getRequest().getSdmCredentials(), @@ -976,9 +977,13 @@ private CmisDocument fetchAttachmentMetadataFromSDM(AttachmentMoveContext moveCo // Create CmisDocument with metadata from SDM CmisDocument cmisDocument = new CmisDocument(); - cmisDocument.setFileName(sdmMetadata.get(0)); // cmis:name - if (sdmMetadata.size() > 1) { - cmisDocument.setDescription(sdmMetadata.get(1)); // cmis:description + JSONObject succinctProperties = sdmMetadata.optJSONObject("succinctProperties"); + if (succinctProperties != null) { + cmisDocument.setFileName(succinctProperties.optString("cmis:name")); // cmis:name + String description = succinctProperties.optString("cmis:description"); + if (description != null && !description.isEmpty()) { + cmisDocument.setDescription(description); // cmis:description + } } // Type and URL will be null for non-link attachments (which is fine for move) return cmisDocument; diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java index 313f77af0..99a0eae5c 100644 --- a/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMServiceGenericHandler.java @@ -356,6 +356,17 @@ public void openAttachment(AttachmentReadContext context) throws Exception { String id = targetKeys.get("ID").toString(); CmisDocument cmisDocument = dbQuery.getObjectIdForAttachmentID(attachmentEntity.get(), persistenceService, id); + if (cmisDocument.getUploadStatus() != null + && cmisDocument + .getUploadStatus() + .equalsIgnoreCase(SDMConstants.UPLOAD_STATUS_VIRUS_DETECTED)) + throw new ServiceException(SDMUtils.getErrorMessage("VIRUS_DETECTED_FILE_ERROR")); + if (cmisDocument.getUploadStatus() != null + && cmisDocument.getUploadStatus().equalsIgnoreCase(SDMConstants.VIRUS_SCAN_INPROGRESS)) + throw new ServiceException(SDMUtils.getErrorMessage("VIRUS_SCAN_IN_PROGRESS_FILE_ERROR")); + if (cmisDocument.getUploadStatus() != null + && cmisDocument.getUploadStatus().equalsIgnoreCase(SDMConstants.UPLOAD_STATUS_IN_PROGRESS)) + throw new ServiceException(SDMUtils.getErrorMessage("UPLOAD_IN_PROGRESS_FILE_ERROR")); if (cmisDocument.getFileName() == null || cmisDocument.getFileName().isEmpty()) { // open attachment is triggered on non-draft entity @@ -363,7 +374,25 @@ public void openAttachment(AttachmentReadContext context) throws Exception { cmisDocument = dbQuery.getObjectIdForAttachmentID(attachmentEntity.get(), persistenceService, id); } + if (cmisDocument.getMimeType().equalsIgnoreCase(SDMConstants.MIMETYPE_INTERNET_SHORTCUT)) { + // Verify access to the object by calling getObject from SDMService + try { + SDMCredentials sdmCredentials = tokenHandler.getSDMCredentials(); + JSONObject objectResponse = + sdmService.getObject( + cmisDocument.getObjectId(), sdmCredentials, context.getUserInfo().isSystemUser()); + + if (objectResponse == null) { + throw new ServiceException(SDMConstants.FILE_NOT_FOUND_ERROR); + } + } catch (ServiceException e) { + if (e.getMessage() != null + && e.getMessage().contains("User does not have required scope")) { + throw new ServiceException(SDMConstants.USER_NOT_AUTHORISED_ERROR_OPEN_LINK); + } + throw e; + } context.setResult(cmisDocument.getUrl()); } else { context.setResult("None"); @@ -403,8 +432,7 @@ private void createLink(EventContext context) throws IOException { Optional parentEntity = parentEntityName != null ? cdsModel.findEntity(parentEntityName) : Optional.empty(); - - String upID = fetchUPIDFromCQN(select, parentEntity.orElse(null)); + String upID = SDMUtils.fetchUPIDFromCQN(select, parentEntity.get()); String filenameInRequest = context.get("name").toString(); Result result = @@ -432,7 +460,8 @@ private void createLink(EventContext context) throws IOException { JSONObject createResult = null; try { - createResult = documentService.createDocument(cmisDocument, sdmCredentials, isSystemUser); + createResult = + documentService.createDocument(cmisDocument, sdmCredentials, isSystemUser, null); } catch (Exception e) { throw new ServiceException( SDMErrorMessages.getGenericError(AttachmentService.EVENT_CREATE_ATTACHMENT), e); @@ -571,6 +600,7 @@ private void handleCreateLinkResult( + cmisDocument.getFolderId() + ":" + context.getTarget()); + updatedFields.put("uploadStatus", SDMConstants.UPLOAD_STATUS_SUCCESS); try { var insert = Insert.into(context.getTarget().getQualifiedName()).entry(updatedFields); @@ -585,39 +615,4 @@ private void handleCreateLinkResult( context.setCompleted(); } } - - private String fetchUPIDFromCQN(CqnSelect select, CdsEntity parentEntity) { - try { - String upID = null; - ObjectMapper mapper = new ObjectMapper(); - JsonNode root = mapper.readTree(select.toString()); - JsonNode refArray = root.path("SELECT").path("from").path("ref"); - JsonNode secondLast = refArray.get(refArray.size() - 2); - JsonNode whereArray = secondLast.path("where"); - - // Get the actual key field names from the parent entity - List keyElementNames = getKeyElementNames(parentEntity); - - for (int i = 0; i < whereArray.size(); i++) { - JsonNode node = whereArray.get(i); - - if (node.has("ref") && node.get("ref").isArray()) { - String fieldName = node.get("ref").get(0).asText(); - - if (keyElementNames.contains(fieldName) && !fieldName.equals("IsActiveEntity")) { - JsonNode valNode = whereArray.get(i + 2); - upID = valNode.path("val").asText(); - break; - } - } - } - if (upID == null) { - throw new ServiceException(SDMUtils.getErrorMessage("ENTITY_PROCESSING_ERROR_LINK")); - } - return upID; - } catch (Exception e) { - logger.error(SDMUtils.getErrorMessage("ENTITY_PROCESSING_ERROR_LINK"), e); - throw new ServiceException(SDMUtils.getErrorMessage("ENTITY_PROCESSING_ERROR_LINK"), e); - } - } } 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 bc00661b5..7ef746932 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,6 +1,12 @@ package com.sap.cds.sdm.utilities; +import static com.sap.cds.sdm.constants.SDMConstants.SDM_READONLY_CONTEXT; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.sap.cds.CdsData; +import com.sap.cds.CdsDataProcessor; +import com.sap.cds.ql.cqn.CqnSelect; import com.sap.cds.reflect.CdsAnnotation; import com.sap.cds.reflect.CdsAssociationType; import com.sap.cds.reflect.CdsElement; @@ -11,6 +17,7 @@ import com.sap.cds.sdm.constants.SDMErrorMessages; import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; import com.sap.cds.sdm.model.AttachmentInfo; +import com.sap.cds.services.ServiceException; import com.sap.cds.services.persistence.PersistenceService; import java.io.IOException; import java.util.ArrayList; @@ -60,6 +67,24 @@ public static Set FileNameContainsWhitespace( return filenamesWithWhitespace; } + public static void cleanupReadonlyContexts(List data) { + for (CdsData entityData : data) { + // Remove SDM_READONLY_CONTEXT from all attachments in the data + entityData.forEach( + (key, value) -> { + if (value instanceof List) { + List list = (List) value; + for (Object item : list) { + if (item instanceof Map) { + Map attachment = (Map) item; + attachment.remove(SDM_READONLY_CONTEXT); + } + } + } + }); + } + } + public static Set FileNameDuplicateInDrafts( List data, String composition, String targetEntity, String upIdKey) { Set uniqueFilenames = new HashSet<>(); @@ -217,7 +242,9 @@ public static Map getPropertyTitles( } CdsEntity entity = attachmentEntity.get(); for (String key : attachment.keySet()) { - if (SDMConstants.DRAFT_READONLY_CONTEXT.equals(key) || entity.getElement(key) == null) { + if (SDMConstants.DRAFT_READONLY_CONTEXT.equals(key) + || SDMConstants.SDM_READONLY_CONTEXT.equals(key) + || entity.getElement(key) == null) { continue; } @@ -232,6 +259,26 @@ public static Map getPropertyTitles( return titleMap; } + public static void preserveReadonlyFields(CdsEntity target, List data) { + CdsDataProcessor.Filter mediaContentFilter = + (path, element, type) -> element.findAnnotation("Core.MediaType").isPresent(); + + CdsDataProcessor.Validator validator = + (path, element, value) -> { + Map values = path.target().values(); + Map readonlyData = new HashMap<>(); + if (values.containsKey("uploadStatus")) { + readonlyData.put("uploadStatus", values.get("uploadStatus")); + } + + if (!readonlyData.isEmpty()) { + values.put(SDM_READONLY_CONTEXT, readonlyData); + } + }; + + CdsDataProcessor.create().addValidator(mediaContentFilter, validator).process(data, target); + } + public static String getErrorMessage(String errorKey) { ErrorMessageKey errorMessageKey = new ErrorMessageKey(); errorMessageKey.setKey(errorKey); @@ -287,7 +334,8 @@ public static Map getSecondaryPropertiesWithInvalidDefinition( if (attachmentEntity.isPresent()) { CdsEntity entity = attachmentEntity.get(); for (String key : keysList) { - if (SDMConstants.DRAFT_READONLY_CONTEXT.equals(key)) { + if (SDMConstants.DRAFT_READONLY_CONTEXT.equals(key) + || SDMConstants.SDM_READONLY_CONTEXT.equals(key)) { continue; // Skip updateProperties processing for DRAFT_READONLY_CONTEXT } CdsElement element = entity.getElement(key); @@ -322,7 +370,8 @@ public static Map getSecondaryTypeProperties( if (attachmentEntity.isPresent()) { CdsEntity entity = attachmentEntity.get(); for (String key : keysList) { - if (SDMConstants.DRAFT_READONLY_CONTEXT.equals(key)) { + if (SDMConstants.DRAFT_READONLY_CONTEXT.equals(key) + || SDMConstants.SDM_READONLY_CONTEXT.equals(key)) { continue; // Skip updateProperties processing for DRAFT_READONLY_CONTEXT } CdsElement element = entity.getElement(key); @@ -466,4 +515,84 @@ private static void retrieveAnnotations(CdsElement cdsElement, AttachmentInfo at annotation -> attachmentInfo.setAttachmentCount(Long.parseLong(annotation.getValue().toString()))); } + + private static List getKeyElementNames(CdsEntity entity) { + return entity.elements().filter(CdsElement::isKey).map(CdsElement::getName).toList(); + } + + /** + * Extracts UP ID from CQN select statement by parsing the JSON representation. + * + * @param select the CQN select statement + * @return the UP ID extracted from the query + * @throws com.sap.cds.services.ServiceException if UP ID cannot be extracted + */ + public static String fetchUPIDFromCQN(CqnSelect select, CdsEntity parentEntity) { + try { + String upID = null; + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(select.toString()); + JsonNode refArray = root.path("SELECT").path("from").path("ref"); + + JsonNode secondLast = refArray.get(refArray.size() - 2); + JsonNode whereArray; + if (secondLast != null) { + whereArray = secondLast.path("where"); + } else { + whereArray = refArray; + } + + // If where condition is not present or empty, return null (valid scenario for select without + // filter) + if (whereArray == null || whereArray.isMissingNode() || whereArray.size() == 0) { + return null; + } + + // Get the actual key field names from the parent entity + List keyElementNames = getKeyElementNames(parentEntity); + + for (int i = 0; i < whereArray.size(); i++) { + JsonNode node = whereArray.get(i); + + if (node.has("ref") && node.get("ref").isArray()) { + String fieldName = node.get("ref").get(0).asText(); + + if (keyElementNames.contains(fieldName) && !fieldName.equals("IsActiveEntity")) { + JsonNode valNode = whereArray.get(i + 2); + upID = valNode.path("val").asText(); + break; + } + } + } + // Return null if UP ID is not found (valid scenario) + return upID; + } catch (Exception e) { + logger.error(SDMConstants.ENTITY_PROCESSING_ERROR_LINK, e); + throw new ServiceException(SDMConstants.ENTITY_PROCESSING_ERROR_LINK, e); + } + } + + /** + * Get criticality value based on upload status for UI display + * + * @param uploadStatus The upload status string + * @return Integer criticality value (1=Error/Red, 2=Warning/Yellow, 3=Success/Green, + * 0=None/Neutral) + */ + public static Integer getCriticalityForStatus(String uploadStatus) { + if (uploadStatus == null) { + return 0; // None/Neutral + } + + switch (uploadStatus) { + case SDMConstants.UPLOAD_STATUS_IN_PROGRESS: + case SDMConstants.VIRUS_SCAN_INPROGRESS: + return 5; // Warning (yellow) + case SDMConstants.UPLOAD_STATUS_VIRUS_DETECTED: + case SDMConstants.UPLOAD_STATUS_FAILED: + return 1; // Error (red) + default: + return 0; // None (neutral) + } + } } diff --git a/sdm/src/main/resources/cds/com.sap.cds/sdm/attachments.cds b/sdm/src/main/resources/cds/com.sap.cds/sdm/attachments.cds index 9d131062b..68a3dfba8 100644 --- a/sdm/src/main/resources/cds/com.sap.cds/sdm/attachments.cds +++ b/sdm/src/main/resources/cds/com.sap.cds/sdm/attachments.cds @@ -1,14 +1,36 @@ namespace sap.attachments; using {sap.attachments.Attachments} from `com.sap.cds/cds-feature-attachments`; +using {sap.attachments.MediaData} from `com.sap.cds/cds-feature-attachments`; +using { + sap.common.CodeList +} from '@sap/cds/common'; + + +type UploadStatusCode : String(32) enum { + uploading; + Success; + Failed; + VirusDetected; + VirusScanInprogress; +} extend aspect Attachments with { folderId : String; repositoryId : String; objectId : String; linkUrl : String default null; type : String @(UI: {IsImageURL: true}) default 'sap-icon://document'; -} + uploadStatus : UploadStatusCode default 'uploading' @readonly ; + uploadStatusNav : Association to one UploadScanStates on uploadStatusNav.code = uploadStatus; + } + entity UploadScanStates : CodeList { + key code : UploadStatusCode @Common.Text: name @Common.TextArrangement: #TextOnly; + name : String(64) ; + criticality : Integer @UI.Hidden; + } + annotate Attachments with @UI: { + HeaderInfo: { $Type : 'UI.HeaderInfoType', TypeName : '{i18n>Attachment}', @@ -16,15 +38,24 @@ annotate Attachments with @UI: { }, LineItem : [ {Value: fileName, @HTML5.CssDefaults: {width: '20%'}}, - {Value: content, @HTML5.CssDefaults: {width: '20%'}}, - {Value: createdAt, @HTML5.CssDefaults: {width: '20%'}}, - {Value: createdBy, @HTML5.CssDefaults: {width: '20%'}}, - {Value: note, @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 : uploadStatus, + Criticality: uploadStatusNav.criticality, + @Common.FieldControl: #ReadOnly, + @HTML5.CssDefaults: {width: '15%'}, + @UI.Hidden: IsActiveEntity + }, ] } { - note @(title: '{i18n>Description}'); + note @(title: '{i18n>Description}', UI.MultiLineText); fileName @(title: '{i18n>Filename}'); - modifiedAt @(odata.etag: null); + modifiedAt @(odata.etag: null); + uploadStatus @(title: '{i18n>uploadStatus}', Common.Text : uploadStatusNav.name, Common.TextArrangement : #TextOnly); content @Core.ContentDisposition: { Filename: fileName, Type: 'inline' } @(title: '{i18n>Attachment}'); @@ -34,7 +65,8 @@ annotate Attachments with @UI: { mimeType @UI.Hidden; status @UI.Hidden; } + annotate Attachments with @Common: {SideEffects #ContentChanged: { SourceProperties: [content], - TargetProperties: ['status'] -}}{}; \ No newline at end of file + TargetProperties: ['uploadStatus'] +}}; \ No newline at end of file diff --git a/sdm/src/main/resources/data/sap.attachments-UploadScanStates.csv b/sdm/src/main/resources/data/sap.attachments-UploadScanStates.csv new file mode 100644 index 000000000..350509058 --- /dev/null +++ b/sdm/src/main/resources/data/sap.attachments-UploadScanStates.csv @@ -0,0 +1,6 @@ +code;name;criticality +uploading;Uploading;5 +Success;Success;3 +Failed;Scan Failed;2 +VirusDetected;Virus detected;1 +VirusScanInprogress;Virus scanning inprogress(refresh page);5 \ No newline at end of file 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 e4992a545..f9c5ff255 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 @@ -12,6 +12,7 @@ import com.sap.cds.sdm.handler.TokenHandler; import com.sap.cds.sdm.handler.applicationservice.SDMCreateAttachmentsHandler; import com.sap.cds.sdm.handler.applicationservice.helper.AttachmentsHandlerUtils; +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; @@ -25,6 +26,7 @@ import java.io.IOException; import java.util.*; import org.ehcache.Cache; +import org.json.JSONObject; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -517,7 +519,15 @@ public void testUpdateNameWithEmptyFilename() throws IOException { () -> AttachmentsHandlerUtils.fetchAttachmentDataFromSDM( any(), anyString(), any(), anyBoolean())) - .thenReturn(Arrays.asList("fileInSDM.txt", "descriptionInSDM")); + .thenReturn( + new JSONObject() + .put("name", "fileInSDM.txt") + .put("description", "descriptionInSDM") + .put( + "succinctProperties", + new JSONObject() + .put("cmis:name", "fileInSDM.txt") + .put("cmis:description", "descriptionInSDM"))); // Mock attachment entity CdsEntity attachmentDraftEntity = mock(CdsEntity.class); @@ -538,8 +548,10 @@ public void testUpdateNameWithEmptyFilename() throws IOException { when(jwtTokenInfo.getToken()).thenReturn("testJwtToken"); // Mock getObject - when(sdmService.getObject("test-object-id", mockCredentials, false)) - .thenReturn(Arrays.asList("fileInSDM.txt", "descriptionInSDM")); + JSONObject mockObject = new JSONObject(); + mockObject.put("name", "fileInSDM.txt"); + mockObject.put("description", "descriptionInSDM"); + when(sdmService.getObject("test-object-id", mockCredentials, false)).thenReturn(mockObject); // Mock getSecondaryTypeProperties Map secondaryTypeProperties = new HashMap<>(); @@ -566,8 +578,12 @@ public void testUpdateNameWithEmptyFilename() throws IOException { .when(() -> SDMUtils.hasRestrictedCharactersInName("fileNameInRequest")) .thenReturn(false); + CmisDocument mockCmisDoc = new CmisDocument(); + mockCmisDoc.setFileName(null); + Map secondaryProps = new HashMap<>(); + mockCmisDoc.setSecondaryProperties(secondaryProps); when(dbQuery.getAttachmentForID(attachmentDraftEntity, persistenceService, "test-id")) - .thenReturn(null); + .thenReturn(mockCmisDoc); // When getPropertiesForID is called when(dbQuery.getPropertiesForID( @@ -660,6 +676,12 @@ public void testUpdateNameWithRestrictedCharacters() throws IOException { data, "compositionName", "some.qualified.Name")) .thenReturn(Arrays.asList("file/1.txt")); + // Mock getAttachmentForID to return CmisDocument + CmisDocument mockCmisDoc = new CmisDocument(); + mockCmisDoc.setFileName("file/1.txt"); + when(dbQuery.getAttachmentForID(attachmentDraftEntity, persistenceService, "test-id")) + .thenReturn(mockCmisDoc); + try (MockedStatic attachmentsHandlerUtilsMocked = mockStatic(AttachmentsHandlerUtils.class)) { attachmentsHandlerUtilsMocked @@ -667,7 +689,15 @@ public void testUpdateNameWithRestrictedCharacters() throws IOException { () -> AttachmentsHandlerUtils.fetchAttachmentDataFromSDM( any(), anyString(), any(), anyBoolean())) - .thenReturn(Arrays.asList("fileInSDM.txt", "descriptionInSDM")); + .thenReturn( + new JSONObject() + .put("name", "fileInSDM.txt") + .put("description", "descriptionInSDM") + .put( + "succinctProperties", + new JSONObject() + .put("cmis:name", "fileInSDM.txt") + .put("cmis:description", "descriptionInSDM"))); attachmentsHandlerUtilsMocked .when( () -> 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 215474ff7..e836da6ff 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 @@ -1,73 +1,211 @@ package unit.com.sap.cds.sdm.handler.applicationservice; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; import com.sap.cds.ql.Select; import com.sap.cds.ql.cqn.CqnSelect; import com.sap.cds.reflect.*; import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.TokenHandler; import com.sap.cds.sdm.handler.applicationservice.SDMReadAttachmentsHandler; +import com.sap.cds.sdm.model.RepoValue; +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.cds.CdsReadEventContext; +import com.sap.cds.services.persistence.PersistenceService; +import com.sap.cds.services.request.UserInfo; +import java.io.IOException; +import java.util.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) public class SDMReadAttachmentsHandlerTest { @Mock private CdsEntity cdsEntity; - @Mock private CdsReadEventContext context; - @Mock private CdsElement mockComposition; - @Mock private CdsAssociationType mockAssociationType; - - @Mock private CdsStructuredType mockTargetAspect; + @Mock private SDMService sdmService; + @Mock private UserInfo userInfo; + @Mock private DBQuery dbQuery; + @Mock private PersistenceService persistenceService; + @Mock private TokenHandler tokenHandler; @InjectMocks private SDMReadAttachmentsHandler sdmReadAttachmentsHandler; - private static final String REPOSITORY_ID_KEY = SDMConstants.REPOSITORY_ID; + private static final String REPOSITORY_ID_KEY = "testRepoId"; @Test - void testModifyCqnForAttachmentsEntity_Success() { + void testModifyCqnForAttachmentsEntity_Success() throws IOException { // Arrange - String targetEntity = "attachments"; CqnSelect select = Select.from(cdsEntity).where(doc -> doc.get("repositoryId").eq(REPOSITORY_ID_KEY)); when(context.getTarget()).thenReturn(cdsEntity); - when(cdsEntity.getAnnotationValue(any(), any())).thenReturn(true); + when(cdsEntity.getAnnotationValue(SDMConstants.ANNOTATION_IS_MEDIA_DATA, false)) + .thenReturn(true); when(context.getCqn()).thenReturn(select); - // Act - sdmReadAttachmentsHandler.processBefore(context); // Refers to the method you provided + 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()); - // Verify the modified where clause - // Predicate whereClause = modifiedCqnSelect.where(); + // Act + sdmReadAttachmentsHandler.processBefore(context); - // Add assertions to validate the modification in `where` clause - assertNotNull(select.where().isPresent()); - assertTrue(select.where().toString().contains("repositoryId")); + // Assert + verify(context).setCqn(any(CqnSelect.class)); + verify(dbQuery) + .updateInProgressUploadStatusToSuccess(any(), any(), eq("mockUpID"), eq("mockUpIdKey")); + } } @Test - void testModifyCqnForNonAttachmentsEntity() { + void testModifyCqnForAttachmentsEntity_Success_TMCheck() throws IOException { // Arrange - String targetEntity = "nonAttachments"; // Does not match the mocked composition name CqnSelect select = - Select.from("SomeEntity").where(doc -> doc.get("repositoryId").eq(REPOSITORY_ID_KEY)); + 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(true); + 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(sdmService.checkRepositoryType(any(), any())).thenReturn(repoValue); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("tenant1"); + when(context.get("cqn")).thenReturn(select); + // Act + try (MockedStatic sdmUtilsMock = Mockito.mockStatic(SDMUtils.class)) { + sdmUtilsMock.when(() -> SDMUtils.getUpIdKey(any())).thenReturn("mockUpIdKey"); + sdmUtilsMock.when(() -> SDMUtils.fetchUPIDFromCQN(any(), any())).thenReturn("mockUpID"); + + // Act + sdmReadAttachmentsHandler.processBefore(context); + + // Assert + // Assert + verify(context).setCqn(any(CqnSelect.class)); + // When async virus scan is enabled, updateInProgressUploadStatusToSuccess is NOT called + verify(dbQuery, never()) + .updateInProgressUploadStatusToSuccess(any(), any(), anyString(), anyString()); + } + } + + @Test + void testModifyCqnForNonAttachmentsEntity() throws IOException { + // Arrange - Mock target to return false for media annotation + when(context.getTarget()).thenReturn(cdsEntity); + when(cdsEntity.getAnnotationValue(SDMConstants.ANNOTATION_IS_MEDIA_DATA, false)) + .thenReturn(false); + + // Act + sdmReadAttachmentsHandler.processBefore(context); + + // Assert — since annotation is false, it should NOT call setCqn + verify(context, never()).setCqn(any()); + verify(context, never()).getCqn(); + } - // Mock target + @Test + void testProcessBefore_ExceptionHandling() throws IOException { + // Arrange when(context.getTarget()).thenReturn(cdsEntity); - when(cdsEntity.getAnnotationValue(any(), any())).thenReturn(false); + when(cdsEntity.getAnnotationValue(SDMConstants.ANNOTATION_IS_MEDIA_DATA, false)) + .thenReturn(true); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("tenant1"); + when(sdmService.checkRepositoryType(any(), any())) + .thenThrow(new RuntimeException("Test exception")); + + // Act & Assert + try { + sdmReadAttachmentsHandler.processBefore(context); + } catch (RuntimeException e) { + // Exception should be re-thrown + verify(sdmService).checkRepositoryType(any(), any()); + } + } + @Test + void testProcessBefore_NoAttachmentDraftEntity() 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); - // Mock composition with Attachments aspect + RepoValue repoValue = new RepoValue(); + repoValue.setIsAsyncVirusScanEnabled(false); + when(sdmService.checkRepositoryType(any(), any())).thenReturn(repoValue); + when(context.getUserInfo()).thenReturn(userInfo); + when(userInfo.getTenant()).thenReturn("tenant1"); + + CdsModel model = Mockito.mock(CdsModel.class); + when(context.getModel()).thenReturn(model); + when(cdsEntity.getQualifiedName()).thenReturn("TestEntity"); + when(model.findEntity(anyString())).thenReturn(Optional.empty()); + when(context.get("cqn")).thenReturn(select); + + // Act + sdmReadAttachmentsHandler.processBefore(context); + + // Assert - should still call setCqn even without draft entity + verify(context).setCqn(any(CqnSelect.class)); + verify(dbQuery, never()) + .updateInProgressUploadStatusToSuccess(any(), any(), anyString(), anyString()); + } + + @Test + void testProcessBefore_WithCollectionReadNoKeys() throws IOException { + // Arrange - create a select without keys (collection read) + CqnSelect select = Select.from("TestEntity"); + 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"); + + CdsModel model = Mockito.mock(CdsModel.class); + when(context.getModel()).thenReturn(model); + when(cdsEntity.getQualifiedName()).thenReturn("TestEntity"); + when(model.findEntity(anyString())).thenReturn(Optional.empty()); + when(context.get("cqn")).thenReturn(select); + // Act sdmReadAttachmentsHandler.processBefore(context); - // Assert — since it enters the 'else' clause, it should call setCqn with original select - verify(context).setCqn(select); + // Assert - repositoryId filter should be added for collection reads + verify(context).setCqn(any(CqnSelect.class)); } } 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 f4fa87a0b..745887f8d 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 @@ -32,6 +32,7 @@ import java.io.IOException; import java.util.*; import org.ehcache.Cache; +import org.json.JSONObject; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; @@ -186,11 +187,14 @@ public void testRenameWithDuplicateFilenames() throws IOException { () -> AttachmentsHandlerUtils.fetchAttachmentDataFromSDM( any(), anyString(), any(), anyBoolean())) - .thenReturn(Arrays.asList("fileInSDM.txt", "descriptionInSDM")); + .thenReturn( + new JSONObject().put("name", "fileInSDM.txt").put("description", "descriptionInSDM")); // Mock dbQuery methods + CmisDocument mockCmisDoc = new CmisDocument(); + mockCmisDoc.setFileName("file1.txt"); when(dbQuery.getAttachmentForID(any(CdsEntity.class), any(PersistenceService.class), any())) - .thenReturn("file1.txt"); + .thenReturn(mockCmisDoc); when(dbQuery.getPropertiesForID( any(CdsEntity.class), any(PersistenceService.class), any(), any(Map.class))) .thenReturn(new HashMap<>()); @@ -362,9 +366,11 @@ public void testRenameWithNoSDMRoles() throws IOException { when(context.getUserInfo()).thenReturn(userInfo); when(userInfo.isSystemUser()).thenReturn(false); when(tokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + CmisDocument mockCmisDoc2 = new CmisDocument(); + mockCmisDoc2.setFileName("file123.txt"); when(dbQuery.getAttachmentForID( any(CdsEntity.class), any(PersistenceService.class), anyString())) - .thenReturn("file123.txt"); + .thenReturn(mockCmisDoc2); when(dbQuery.getPropertiesForID( any(CdsEntity.class), any(PersistenceService.class), anyString(), any(Map.class))) diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/service/DocumentUploadServiceTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/service/DocumentUploadServiceTest.java deleted file mode 100644 index 6a20763a3..000000000 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/service/DocumentUploadServiceTest.java +++ /dev/null @@ -1,1049 +0,0 @@ -package unit.com.sap.cds.sdm.service; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -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.service.DocumentUploadService; -import com.sap.cds.services.environment.CdsProperties; -import com.sap.cloud.environment.servicebinding.api.ServiceBinding; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import org.apache.http.HttpEntity; -import org.apache.http.StatusLine; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.util.EntityUtils; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.MockitoAnnotations; - -class DocumentUploadServiceTest { - - @Mock private ServiceBinding serviceBinding; - @Mock private CdsProperties.ConnectionPool connectionPool; - @Mock private TokenHandler tokenHandler; - @Mock private HttpClient httpClient; - @Mock private CloseableHttpResponse httpResponse; - @Mock private StatusLine statusLine; - @Mock private HttpEntity httpEntity; - - private DocumentUploadService documentUploadService; - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - documentUploadService = new DocumentUploadService(serviceBinding, connectionPool, tokenHandler); - } - - @Test - void testDocumentUploadServiceConstructor() { - // Then - assertNotNull(documentUploadService); - } - - @Test - void testDocumentUploadServiceWithNullBinding() { - // When & Then - assertDoesNotThrow( - () -> { - new DocumentUploadService(null, connectionPool, tokenHandler); - }); - } - - @Test - void testDocumentUploadServiceWithNullConnectionPool() { - // When & Then - assertDoesNotThrow( - () -> { - new DocumentUploadService(serviceBinding, null, tokenHandler); - }); - } - - @Test - void testDocumentUploadServiceWithNullTokenHandler() { - // When & Then - assertDoesNotThrow( - () -> { - new DocumentUploadService(serviceBinding, connectionPool, null); - }); - } - - @Test - void testDocumentUploadServiceAllNullParameters() { - // When & Then - assertDoesNotThrow( - () -> { - new DocumentUploadService(null, null, null); - }); - } - - @Test - void testCreateDocumentWithInternetShortcut() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setMimeType("application/internet-shortcut"); - cmisDocument.setUrl("https://example.com"); - cmisDocument.setContentLength(100); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(201); - when(httpResponse.getEntity()).thenReturn(httpEntity); - when(httpEntity.toString()) - .thenReturn( - "{\"succinctProperties\":{\"cmis:objectId\":\"12345\",\"cmis:contentStreamMimeType\":\"application/internet-shortcut\"}}"); - - // When - IOException exception = - assertThrows( - IOException.class, - () -> { - documentUploadService.createDocument(cmisDocument, sdmCredentials, false); - }); - - // Then - assertNotNull(exception); - assertTrue(exception.getMessage().contains("Error uploading document")); - } - - @Test - void testCreateDocumentSmallFile() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContentLength(100 * 1024); // 100KB - should use single chunk - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(201); - when(httpResponse.getEntity()).thenReturn(httpEntity); - when(httpEntity.toString()) - .thenReturn( - "{\"succinctProperties\":{\"cmis:objectId\":\"12345\",\"cmis:contentStreamMimeType\":\"text/plain\"}}"); - - // When - IOException exception = - assertThrows( - IOException.class, - () -> { - documentUploadService.createDocument(cmisDocument, sdmCredentials, false); - }); - - // Then - assertNotNull(exception); - assertTrue(exception.getMessage().contains("Error uploading document")); - } - - @Test - void testCreateDocumentLargeFile() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContentLength(500 * 1024 * 1024); // 500MB - should use chunked upload - byte[] largeContent = new byte[500 * 1024 * 1024]; - cmisDocument.setContent(new ByteArrayInputStream(largeContent)); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - // When - IOException exception = - assertThrows( - IOException.class, - () -> { - documentUploadService.createDocument(cmisDocument, sdmCredentials, false); - }); - - // Then - assertNotNull(exception); - assertTrue(exception.getMessage().contains("Error uploading document")); - } - - @Test - void testCreateDocumentWithNullContent() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(null); - cmisDocument.setContentLength(100); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - // When - IOException exception = - assertThrows( - IOException.class, - () -> { - documentUploadService.createDocument(cmisDocument, sdmCredentials, false); - }); - - // Then - assertNotNull(exception); - assertTrue(exception.getMessage().contains("Error uploading document")); - } - - @Test - void testUploadSingleChunkWithNullStream() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(null); - cmisDocument.setMimeType("text/plain"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - // When - IOException exception = - assertThrows( - IOException.class, - () -> { - documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - }); - - // Then - assertNotNull(exception); - assertEquals("File stream is null!", exception.getMessage()); - } - - @Test - void testUploadSingleChunkSuccess() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - cmisDocument.setMimeType("text/plain"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(201); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse = - "{\"succinctProperties\":{\"cmis:objectId\":\"12345\",\"cmis:contentStreamMimeType\":\"text/plain\"}}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(jsonResponse); - - // When & Then - assertDoesNotThrow( - () -> { - documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - }); - } - } - - @Test - void testUploadSingleChunkWithInternetShortcut() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setMimeType("application/internet-shortcut"); - cmisDocument.setUrl("https://example.com"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(201); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse = - "{\"succinctProperties\":{\"cmis:objectId\":\"12345\",\"cmis:contentStreamMimeType\":\"application/internet-shortcut\"}}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(jsonResponse); - - // When & Then - assertDoesNotThrow( - () -> { - documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - }); - } - } - - @Test - void testExecuteHttpPostWithIOException() throws Exception { - // This test validates the executeHttpPost method's exception handling - // We can't easily test this private method directly, but it's covered by other tests - // that call createDocument or uploadSingleChunk - assertTrue(true); // This test passes by design as the method is private - } - - @Test - void testCreateDocumentWithSystemUser() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setMimeType("application/internet-shortcut"); - cmisDocument.setUrl("https://example.com"); - cmisDocument.setContentLength(100); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), eq("TECHNICAL_CREDENTIALS_FLOW"))) - .thenReturn(httpClient); - - // When - IOException exception = - assertThrows( - IOException.class, - () -> { - documentUploadService.createDocument(cmisDocument, sdmCredentials, true); - }); - - // Then - assertNotNull(exception); - verify(tokenHandler).getHttpClient(any(), any(), any(), eq("TECHNICAL_CREDENTIALS_FLOW")); - } - - @Test - void testCreateDocumentWithNamedUser() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setMimeType("application/internet-shortcut"); - cmisDocument.setUrl("https://example.com"); - cmisDocument.setContentLength(100); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) - .thenReturn(httpClient); - - // When - IOException exception = - assertThrows( - IOException.class, - () -> { - documentUploadService.createDocument(cmisDocument, sdmCredentials, false); - }); - - // Then - assertNotNull(exception); - verify(tokenHandler).getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE")); - } - - @Test - void testServiceInstantiation() { - // Given - ServiceBinding mockBinding = mock(ServiceBinding.class); - CdsProperties.ConnectionPool mockPool = mock(CdsProperties.ConnectionPool.class); - TokenHandler mockTokenHandler = mock(TokenHandler.class); - - // When - DocumentUploadService service = - new DocumentUploadService(mockBinding, mockPool, mockTokenHandler); - - // Then - assertNotNull(service); - } - - @Test - void testFormResponseWithSuccessfulUpload() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - cmisDocument.setMimeType("text/plain"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(201); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse = - "{\"succinctProperties\":{\"cmis:objectId\":\"12345\",\"cmis:contentStreamMimeType\":\"text/plain\"}}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(jsonResponse); - - // When - var result = documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - - // Then - assertNotNull(result); - assertEquals("success", result.getString("status")); - assertEquals("12345", result.getString("objectId")); - } - } - - @Test - void testFormResponseWithDuplicateError() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - cmisDocument.setMimeType("text/plain"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(409); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse = "{\"message\":\"Document already exists\"}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(jsonResponse); - - // When - var result = documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - - // Then - assertNotNull(result); - assertEquals("duplicate", result.getString("status")); - } - } - - @Test - void testFormResponseWithVirusDetected() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - cmisDocument.setMimeType("text/plain"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(409); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse = "{\"message\":\"Malware Service Exception: Virus found in the file!\"}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(jsonResponse); - - // When - var result = documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - - // Then - assertNotNull(result); - assertEquals("virus", result.getString("status")); - } - } - - @Test - void testFormResponseWithUnauthorizedError() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - cmisDocument.setMimeType("text/plain"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(403); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils - .when(() -> EntityUtils.toString(httpEntity)) - .thenReturn("User does not have required scope"); - - // When - var result = documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - - // Then - assertNotNull(result); - assertEquals("unauthorized", result.getString("status")); - } - } - - @Test - void testFormResponseWithBlockedMimeType() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - cmisDocument.setMimeType("application/x-executable"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(403); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse = - "{\"message\":\"MIME type of the uploaded file is blocked according to your repository configuration.\"}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(jsonResponse); - - // When - var result = documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - - // Then - assertNotNull(result); - assertEquals("blocked", result.getString("status")); - } - } - - @Test - void testFormResponseWithGenericError() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - cmisDocument.setMimeType("text/plain"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(500); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse = "{\"message\":\"Internal server error\"}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(jsonResponse); - - // When - var result = documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - - // Then - assertNotNull(result); - assertEquals("fail", result.getString("status")); - assertEquals("Internal server error", result.getString("message")); - } - } - - @Test - void testCreateDocumentExceptionHandling() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setMimeType("text/plain"); - cmisDocument.setContentLength(100); - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())) - .thenThrow(new RuntimeException("Token error")); - - // When - IOException exception = - assertThrows( - IOException.class, - () -> { - documentUploadService.createDocument(cmisDocument, sdmCredentials, false); - }); - - // Then - assertNotNull(exception); - assertTrue(exception.getMessage().contains("Error uploading document")); - assertTrue(exception.getCause().getMessage().contains("Token error")); - } - - @Test - void testCreateDocumentBoundarySize() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContentLength(400 * 1024 * 1024); // Exactly 400MB - should use single chunk - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(201); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse = - "{\"succinctProperties\":{\"cmis:objectId\":\"12345\",\"cmis:contentStreamMimeType\":\"text/plain\"}}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(jsonResponse); - - // When - Should attempt single chunk upload for exactly 400MB - var result = documentUploadService.createDocument(cmisDocument, sdmCredentials, false); - - // Then - assertNotNull(result); - assertEquals("success", result.getString("status")); - } - } - - @Test - void testUploadSingleChunkWith200StatusCode() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - cmisDocument.setMimeType("text/plain"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(200); // Test 200 instead of 201 - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse = - "{\"succinctProperties\":{\"cmis:objectId\":\"12345\",\"cmis:contentStreamMimeType\":\"text/plain\"}}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(jsonResponse); - - // When - var result = documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - - // Then - assertNotNull(result); - assertEquals("success", result.getString("status")); - assertEquals("12345", result.getString("objectId")); - } - } - - private CmisDocument createTestCmisDocument() { - return CmisDocument.builder() - .attachmentId("att123") - .fileName("test.txt") - .folderId("folder123") - .repositoryId("repo123") - .mimeType("text/plain") - .contentLength(100) - .build(); - } - - private SDMCredentials createTestSDMCredentials() { - return SDMCredentials.builder() - .url("https://sdm.example.com/") - .clientId("testClientId") - .clientSecret("testClientSetcret") - .baseTokenUrl("https://token.example.com/") - .build(); - } - - @Test - void testFormResponseWithSuccessAndNoMimeType() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - cmisDocument.setMimeType("text/plain"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(201); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse = "{\"succinctProperties\":{\"cmis:objectId\":\"12345\"}}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(jsonResponse); - - // When - var result = documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - - // Then - assertNotNull(result); - assertEquals("success", result.getString("status")); - assertEquals("12345", result.getString("objectId")); - // Check if mimeType key exists in response - assertFalse( - result.has( - "mimeType")); // Since objectId is not empty but mimeType is null, it won't be added - } - } - - @Test - void testFormResponseWithOtherForbiddenError() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - cmisDocument.setMimeType("text/plain"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(403); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse = "{\"message\":\"Access denied for other reason\"}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(jsonResponse); - - // When - var result = documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - - // Then - assertNotNull(result); - assertEquals( - "success", result.getString("status")); // Status remains success for unhandled 403 cases - assertEquals("", result.getString("message")); // Message is empty since error wasn't set - } - } - - @Test - void testCreateDocumentJustOverBoundarySize() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContentLength(400 * 1024 * 1024 + 1); // Just over 400MB - should use chunked - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - // When - IOException exception = - assertThrows( - IOException.class, - () -> { - documentUploadService.createDocument(cmisDocument, sdmCredentials, false); - }); - - // Then - assertNotNull(exception); - assertTrue(exception.getMessage().contains("Error uploading document")); - } - - @Test - void testUploadSingleChunkWithExecuteHttpPostIOException() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - cmisDocument.setMimeType("text/plain"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenThrow(new IOException("Network error")); - - // When & Then - assertThrows( - com.sap.cds.services.ServiceException.class, - () -> { - documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - }); - } - - @Test - void testFormResponseWithIOExceptionInEntityUtils() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - cmisDocument.setMimeType("text/plain"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(201); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils - .when(() -> EntityUtils.toString(httpEntity)) - .thenThrow(new IOException("Failed to read response")); - - // When & Then - assertThrows( - com.sap.cds.services.ServiceException.class, - () -> { - documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - }); - } - } - - @Test - void testCreateDocumentWithLargeFileAndIOException() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContentLength(500 * 1024 * 1024); // 500MB - should use chunked upload - cmisDocument.setContent( - null); // This will cause issues when trying to create ReadAheadInputStream - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - // When - IOException exception = - assertThrows( - IOException.class, - () -> { - documentUploadService.createDocument(cmisDocument, sdmCredentials, false); - }); - - // Then - assertNotNull(exception); - assertTrue(exception.getMessage().contains("Error uploading document")); - } - - @Test - void testUploadSingleChunkWithSystemUserFlow() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - cmisDocument.setMimeType("text/plain"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), eq("TECHNICAL_CREDENTIALS_FLOW"))) - .thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(201); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse = - "{\"succinctProperties\":{\"cmis:objectId\":\"12345\",\"cmis:contentStreamMimeType\":\"text/plain\"}}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(jsonResponse); - - // When - var result = documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, true); - - // Then - assertNotNull(result); - assertEquals("success", result.getString("status")); - verify(tokenHandler).getHttpClient(any(), any(), any(), eq("TECHNICAL_CREDENTIALS_FLOW")); - } - } - - @Test - void testUploadSingleChunkWithNamedUserFlow() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - cmisDocument.setMimeType("text/plain"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) - .thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(201); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse = - "{\"succinctProperties\":{\"cmis:objectId\":\"12345\",\"cmis:contentStreamMimeType\":\"text/plain\"}}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(jsonResponse); - - // When - var result = documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - - // Then - assertNotNull(result); - assertEquals("success", result.getString("status")); - verify(tokenHandler).getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE")); - } - } - - @Test - void testCreateDocumentWithEdgeCaseMimeTypes() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setMimeType("APPLICATION/INTERNET-SHORTCUT"); // Test case sensitivity - cmisDocument.setUrl("https://example.com"); - cmisDocument.setContentLength(100); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - - // When - IOException exception = - assertThrows( - IOException.class, - () -> { - documentUploadService.createDocument(cmisDocument, sdmCredentials, false); - }); - - // Then - assertNotNull(exception); - assertTrue(exception.getMessage().contains("Error uploading document")); - } - - @Test - void testUploadSingleChunkWithLinkAndNullContent() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setMimeType("application/internet-shortcut"); - cmisDocument.setUrl("https://example.com"); - cmisDocument.setContent(null); // Should be fine for links - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(201); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse = - "{\"succinctProperties\":{\"cmis:objectId\":\"12345\",\"cmis:contentStreamMimeType\":\"application/internet-shortcut\"}}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(jsonResponse); - - // When & Then - assertDoesNotThrow( - () -> { - documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - }); - } - } - - @Test - void testCreateDocumentWithZeroSizeFile() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContentLength(0); // Zero size should use single chunk - cmisDocument.setContent(new ByteArrayInputStream(new byte[0])); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(201); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse = - "{\"succinctProperties\":{\"cmis:objectId\":\"12345\",\"cmis:contentStreamMimeType\":\"text/plain\"}}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(jsonResponse); - - // When - var result = documentUploadService.createDocument(cmisDocument, sdmCredentials, false); - - // Then - assertNotNull(result); - assertEquals("success", result.getString("status")); - } - } - - @Test - void testFormResponseWithMalformedJSON() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - cmisDocument.setMimeType("text/plain"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(500); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String malformedResponse = "{invalid json}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(malformedResponse); - - // When & Then - assertThrows( - Exception.class, - () -> { - documentUploadService.uploadSingleChunk(cmisDocument, sdmCredentials, false); - }); - } - } - - @Test - void testCreateDocumentWithNegativeContentLength() throws Exception { - // Given - CmisDocument cmisDocument = createTestCmisDocument(); - cmisDocument.setContentLength(-1); // Invalid content length - cmisDocument.setContent(new ByteArrayInputStream("test content".getBytes())); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(201); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse = - "{\"succinctProperties\":{\"cmis:objectId\":\"12345\",\"cmis:contentStreamMimeType\":\"text/plain\"}}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils.when(() -> EntityUtils.toString(httpEntity)).thenReturn(jsonResponse); - - // When - var result = documentUploadService.createDocument(cmisDocument, sdmCredentials, false); - - // Then - Should handle negative content length gracefully - assertNotNull(result); - } - } - - @Test - void testMultipleConsecutiveCalls() throws Exception { - // Given - CmisDocument cmisDocument1 = createTestCmisDocument(); - cmisDocument1.setContent(new ByteArrayInputStream("test content 1".getBytes())); - cmisDocument1.setAttachmentId("att1"); - cmisDocument1.setFileName("file1.txt"); - - CmisDocument cmisDocument2 = createTestCmisDocument(); - cmisDocument2.setContent(new ByteArrayInputStream("test content 2".getBytes())); - cmisDocument2.setAttachmentId("att2"); - cmisDocument2.setFileName("file2.txt"); - - SDMCredentials sdmCredentials = createTestSDMCredentials(); - - when(tokenHandler.getHttpClient(any(), any(), any(), any())).thenReturn(httpClient); - when(httpClient.execute(any(HttpPost.class))).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(statusLine); - when(statusLine.getStatusCode()).thenReturn(201); - when(httpResponse.getEntity()).thenReturn(httpEntity); - - String jsonResponse1 = - "{\"succinctProperties\":{\"cmis:objectId\":\"obj1\",\"cmis:contentStreamMimeType\":\"text/plain\"}}"; - String jsonResponse2 = - "{\"succinctProperties\":{\"cmis:objectId\":\"obj2\",\"cmis:contentStreamMimeType\":\"text/plain\"}}"; - - try (MockedStatic mockedEntityUtils = mockStatic(EntityUtils.class)) { - mockedEntityUtils - .when(() -> EntityUtils.toString(httpEntity)) - .thenReturn(jsonResponse1) - .thenReturn(jsonResponse2); - - // When - var result1 = documentUploadService.uploadSingleChunk(cmisDocument1, sdmCredentials, false); - var result2 = documentUploadService.uploadSingleChunk(cmisDocument2, sdmCredentials, false); - - // Then - assertNotNull(result1); - assertNotNull(result2); - assertEquals("success", result1.getString("status")); - assertEquals("success", result2.getString("status")); - assertEquals("obj1", result1.getString("objectId")); - assertEquals("obj2", result2.getString("objectId")); - assertEquals("att1", result1.getString("id")); - assertEquals("att2", result2.getString("id")); - } - } -} 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 b63f0b4d8..6233883f4 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 @@ -226,6 +226,7 @@ public void testCheckRepositoryTypeNoCacheVersioned() throws IOException { JSONObject featureData = new JSONObject(); featureData.put("virusScanner", "false"); featureData.put("disableVirusScannerForLargeFile", "false"); + featureData.put("isAsyncVirusScanEnabled", "false"); // Create a JSON object representing an 'extendedFeature' entry with 'featureData' JSONObject extendedFeatureWithVirusScanner = new JSONObject(); extendedFeatureWithVirusScanner.put("id", "ecmRepoInfo"); @@ -283,6 +284,7 @@ public void testCheckRepositoryTypeNoCacheNonVersioned() throws IOException { JSONObject featureData = new JSONObject(); featureData.put("virusScanner", "false"); featureData.put("disableVirusScannerForLargeFile", "false"); + featureData.put("isAsyncVirusScanEnabled", "false"); // Create a JSON object representing an 'extendedFeature' entry with 'featureData' JSONObject extendedFeatureWithVirusScanner = new JSONObject(); @@ -1465,8 +1467,9 @@ public void testGetObject_Success() throws IOException { InputStream inputStream = new ByteArrayInputStream(mockResponseBody.getBytes()); when(entity.getContent()).thenReturn(inputStream); - List objectInfo = sdmServiceImpl.getObject(objectId, sdmCredentials, false); - assertEquals("desiredObjectName", objectInfo.get(0)); + JSONObject objectInfo = sdmServiceImpl.getObject(objectId, sdmCredentials, false); + assertEquals( + "desiredObjectName", objectInfo.getJSONObject("succinctProperties").getString("cmis:name")); } @Test @@ -1485,8 +1488,8 @@ public void testGetObject_Failure() throws IOException { InputStream inputStream = new ByteArrayInputStream("".getBytes()); when(entity.getContent()).thenReturn(inputStream); - List objectInfo = sdmServiceImpl.getObject(objectId, sdmCredentials, false); - assertTrue(objectInfo.isEmpty()); + JSONObject objectInfo = sdmServiceImpl.getObject(objectId, sdmCredentials, false); + assertNull(objectInfo); } @Test diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandlerTest.java index f09f04e17..4d4f984cc 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandlerTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandlerTest.java @@ -289,6 +289,7 @@ public void testCreateVirusEnabled() throws IOException { repoValue.setVirusScanEnabled(true); repoValue.setDisableVirusScannerForLargeFile(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); when(mockContext.getMessages()).thenReturn(mockMessages); when(mockMessages.error(SDMUtils.getErrorMessage("VIRUS_REPO_ERROR_MORE_THAN_400MB"))) @@ -354,6 +355,7 @@ public void testCreateNonVersionedDuplicate() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); when(mockResult.list()).thenReturn(nonEmptyRowList); when(mockContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); @@ -429,6 +431,7 @@ public void testCreateNonVersionedDIDuplicate() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); when(mockContext.getData()).thenReturn(mockMediaData); when(mockContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); @@ -439,7 +442,11 @@ public void testCreateNonVersionedDIDuplicate() throws IOException { mockResponse.put("status", "duplicate"); // Mock the behavior of createDocumentRx to return the mock response wrapped in a Single - when(documentUploadService.createDocument(any(), any(), anyBoolean())) + when(documentUploadService.createDocument( + any(CmisDocument.class), + any(SDMCredentials.class), + anyBoolean(), + any(AttachmentCreateEventContext.class))) .thenReturn(mockCreateResult); when(mockResult.list()).thenReturn(nonEmptyRowList); doReturn(false).when(handlerSpy).duplicateCheck(any(), any(), any()); @@ -524,6 +531,7 @@ public void testCreateNonVersionedDIVirus() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); when(mockResult.list()).thenReturn(nonEmptyRowList); when(mockContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); @@ -536,7 +544,11 @@ public void testCreateNonVersionedDIVirus() throws IOException { mockResponse.put("status", "virus"); // Mock the behavior of createDocumentRx to return the mock response wrapped in a Single - when(documentUploadService.createDocument(any(), any(), anyBoolean())) + when(documentUploadService.createDocument( + any(CmisDocument.class), + any(SDMCredentials.class), + anyBoolean(), + any(AttachmentCreateEventContext.class))) .thenReturn(mockCreateResult); ParameterInfo mockParameterInfo = mock(ParameterInfo.class); Map mockHeaders = new HashMap<>(); @@ -679,6 +691,7 @@ public void testCreateNonVersionedDIOther() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); when(mockResult.list()).thenReturn(nonEmptyRowList); when(mockContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); @@ -691,7 +704,11 @@ public void testCreateNonVersionedDIOther() throws IOException { mockResponse.put("status", "fail"); mockResponse.put("message", "Failed due to a DI error"); // Mock the behavior of createDocumentRx to return the mock response wrapped in a Single - when(documentUploadService.createDocument(any(), any(), anyBoolean())) + when(documentUploadService.createDocument( + any(CmisDocument.class), + any(SDMCredentials.class), + anyBoolean(), + any(AttachmentCreateEventContext.class))) .thenReturn(mockCreateResult); try (MockedStatic sdmUtilsMockedStatic = mockStatic(SDMUtils.class, CALLS_REAL_METHODS); ) { @@ -757,6 +774,7 @@ public void testCreateNonVersionedDIUnauthorizedI18n() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(mockContext.getUserInfo()).thenReturn(userInfo); when(userInfo.getTenant()).thenReturn("t1"); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); @@ -774,7 +792,11 @@ public void testCreateNonVersionedDIUnauthorizedI18n() throws IOException { when(mockMediaData.getFileName()).thenReturn("test.txt"); // Mock the behavior of createDocument and other dependencies - when(documentUploadService.createDocument(any(), any(), anyBoolean())) + when(documentUploadService.createDocument( + any(CmisDocument.class), + any(SDMCredentials.class), + anyBoolean(), + any(AttachmentCreateEventContext.class))) .thenReturn(mockCreateResult); doReturn(false).when(handlerSpy).duplicateCheck(any(), any(), any()); when(dbQuery.getAttachmentsForUPID(any(), any(), any(), any())).thenReturn(mockResult); @@ -846,6 +868,7 @@ public void testCreateNonVersionedDIUnauthorized() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(mockContext.getUserInfo()).thenReturn(userInfo); when(userInfo.getTenant()).thenReturn("t1"); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); @@ -863,7 +886,11 @@ public void testCreateNonVersionedDIUnauthorized() throws IOException { when(mockJwtTokenInfo.getToken()).thenReturn("mockedJwtToken"); // Mock the behavior of createDocument and other dependencies - when(documentUploadService.createDocument(any(), any(), anyBoolean())) + when(documentUploadService.createDocument( + any(CmisDocument.class), + any(SDMCredentials.class), + anyBoolean(), + any(AttachmentCreateEventContext.class))) .thenReturn(mockCreateResult); doReturn(false).when(handlerSpy).duplicateCheck(any(), any(), any()); when(dbQuery.getAttachmentsForUPID(any(), any(), any(), any())).thenReturn(mockResult); @@ -940,6 +967,7 @@ public void testCreateNonVersionedDIBlocked() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); repoValue.setDisableVirusScannerForLargeFile(false); when(mockContext.getUserInfo()).thenReturn(userInfo); when(userInfo.getTenant()).thenReturn("t1"); @@ -958,7 +986,11 @@ public void testCreateNonVersionedDIBlocked() throws IOException { when(mockJwtTokenInfo.getToken()).thenReturn("mockedJwtToken"); // Mock the behavior of createDocument and other dependencies - when(documentUploadService.createDocument(any(), any(), anyBoolean())) + when(documentUploadService.createDocument( + any(CmisDocument.class), + any(SDMCredentials.class), + anyBoolean(), + any(AttachmentCreateEventContext.class))) .thenReturn(mockCreateResult); doReturn(false).when(handlerSpy).duplicateCheck(any(), any(), any()); when(dbQuery.getAttachmentsForUPID(any(), any(), any(), any())).thenReturn(mockResult); @@ -1022,7 +1054,7 @@ public void testCreateNonVersionedDISuccess() throws IOException { mockCreateResult.put("name", "sample.pdf"); mockCreateResult.put("objectId", "objectId"); mockCreateResult.put("mimeType", "application/pdf"); - + mockCreateResult.put("uploadStatus", "Success"); when(mockMediaData.getFileName()).thenReturn("sample.pdf"); when(mockMediaData.getContent()).thenReturn(contentStream); when(mockContext.getModel()).thenReturn(cdsModel); @@ -1037,6 +1069,7 @@ public void testCreateNonVersionedDISuccess() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); when(mockResult.list()).thenReturn(nonEmptyRowList); when(mockContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); @@ -1050,7 +1083,11 @@ public void testCreateNonVersionedDISuccess() throws IOException { mockResponse.put("objectId", "123"); // Mock the behavior of createDocumentRx to return the mock response wrapped in a Single - when(documentUploadService.createDocument(any(), any(), anyBoolean())) + when(documentUploadService.createDocument( + any(CmisDocument.class), + any(SDMCredentials.class), + anyBoolean(), + any(AttachmentCreateEventContext.class))) .thenReturn(mockCreateResult); ParameterInfo mockParameterInfo = mock(ParameterInfo.class); Map mockHeaders = new HashMap<>(); @@ -1103,6 +1140,7 @@ public void testCreateVirusEnabledDisableLargeFileDISuccess() throws IOException mockCreateResult.put("name", "sample.pdf"); mockCreateResult.put("objectId", "objectId"); mockCreateResult.put("mimeType", "application/pdf"); + mockCreateResult.put("uploadStatus", "Success"); when(mockMediaData.getFileName()).thenReturn("sample.pdf"); when(mockMediaData.getContent()).thenReturn(contentStream); when(mockContext.getModel()).thenReturn(cdsModel); @@ -1118,6 +1156,7 @@ public void testCreateVirusEnabledDisableLargeFileDISuccess() throws IOException repoValue.setVirusScanEnabled(true); repoValue.setVersionEnabled(false); repoValue.setDisableVirusScannerForLargeFile(true); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); when(mockResult.list()).thenReturn(nonEmptyRowList); when(mockContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); @@ -1131,7 +1170,11 @@ public void testCreateVirusEnabledDisableLargeFileDISuccess() throws IOException mockResponse.put("objectId", "123"); mockResponse.put("mimeType", "application/pdf"); // Mock the behavior of createDocumentRx to return the mock response wrapped in a Single - when(documentUploadService.createDocument(any(), any(), anyBoolean())) + when(documentUploadService.createDocument( + any(CmisDocument.class), + any(SDMCredentials.class), + anyBoolean(), + any(AttachmentCreateEventContext.class))) .thenReturn(mockCreateResult); ParameterInfo mockParameterInfo = mock(ParameterInfo.class); Map mockHeaders = new HashMap<>(); @@ -1189,6 +1232,7 @@ public void testCreateNonVersionedNoUpAssociation() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); when(mockResult.list()).thenReturn(nonEmptyRowList); when(mockContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); @@ -1299,6 +1343,7 @@ public void testCreateNonVersionedNameConstraint() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); when(mockResult.list()).thenReturn(nonEmptyRowList); when(mockContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); @@ -1519,10 +1564,13 @@ public void testReadAttachment_NotVersionedRepository() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); SDMCredentials mockSdmCredentials = new SDMCredentials(); when(tokenHandler.getSDMCredentials()).thenReturn(mockSdmCredentials); - + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setUploadStatus(SDMConstants.UPLOAD_STATUS_SUCCESS); + when(dbQuery.getuploadStatusForAttachment(any(), any(), any(), any())).thenReturn(cmisDocument); handlerSpy.readAttachment(mockReadContext); // Verify that readDocument method was called @@ -1538,13 +1586,16 @@ public void testReadAttachment_FailureInReadDocument() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); SDMCredentials mockSdmCredentials = new SDMCredentials(); when(tokenHandler.getSDMCredentials()).thenReturn(mockSdmCredentials); doThrow(new ServiceException("FILE_NOT_FOUND_ERROR")) .when(sdmService) .readDocument(anyString(), any(SDMCredentials.class), eq(mockReadContext)); - + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setUploadStatus(SDMConstants.UPLOAD_STATUS_SUCCESS); + when(dbQuery.getuploadStatusForAttachment(any(), any(), any(), any())).thenReturn(cmisDocument); ServiceException exception = assertThrows( ServiceException.class, @@ -1593,6 +1644,7 @@ public void testMaxCountErrorMessagei18n() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); when(mockResult.list()).thenReturn(nonEmptyRowList); when(mockResult.rowCount()).thenReturn(3L); @@ -1668,6 +1720,7 @@ public void testMaxCountErrorMessage() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); when(mockResult.list()).thenReturn(nonEmptyRowList); when(mockResult.rowCount()).thenReturn(3L); @@ -1743,6 +1796,7 @@ public void testMaxCountError() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); when(mockResult.list()).thenReturn(nonEmptyRowList); when(mockResult.rowCount()).thenReturn(3L); @@ -1789,6 +1843,8 @@ public void throwAttachmetDraftEntityException() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); when(mockContext.getModel()).thenReturn(cdsModel); when(mockContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); @@ -1816,6 +1872,7 @@ public void testCreateAttachment_WithNullContentLength() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); Map emptyHeaders = new HashMap<>(); @@ -1844,6 +1901,7 @@ public void testCreateAttachment_WithEmptyContentLength() throws IOException { RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); Map headersWithEmpty = new HashMap<>(); @@ -1874,6 +1932,7 @@ public void testCreateAttachment_VirusScanEnabledExceedsLimit() throws IOExcepti repoValue.setVirusScanEnabled(true); repoValue.setVersionEnabled(false); repoValue.setDisableVirusScannerForLargeFile(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); Map largeFileHeaders = new HashMap<>(); @@ -1905,6 +1964,7 @@ public void testCreateAttachment_VirusScanEnabledWithinLimit() throws IOExceptio repoValue.setVirusScanEnabled(true); repoValue.setVersionEnabled(false); repoValue.setDisableVirusScannerForLargeFile(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); Map normalFileHeaders = new HashMap<>(); @@ -1935,6 +1995,7 @@ public void testCreateAttachment_VirusScanDisabledForLargeFile() throws IOExcept repoValue.setVirusScanEnabled(true); repoValue.setVersionEnabled(false); repoValue.setDisableVirusScannerForLargeFile(true); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); Map largeFileHeaders = new HashMap<>(); @@ -2079,7 +2140,9 @@ public void testReadAttachment_ValidContentId() throws IOException { SDMCredentials mockSdmCredentials = new SDMCredentials(); when(tokenHandler.getSDMCredentials()).thenReturn(mockSdmCredentials); - + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setUploadStatus(SDMConstants.UPLOAD_STATUS_SUCCESS); + when(dbQuery.getuploadStatusForAttachment(any(), any(), any(), any())).thenReturn(cmisDocument); handlerSpy.readAttachment(readContext); verify(sdmService).readDocument(eq("objectId"), eq(mockSdmCredentials), eq(readContext)); @@ -2096,7 +2159,9 @@ public void testReadAttachment_InvalidContentId() throws IOException { SDMCredentials mockSdmCredentials = new SDMCredentials(); when(tokenHandler.getSDMCredentials()).thenReturn(mockSdmCredentials); - + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setUploadStatus(SDMConstants.UPLOAD_STATUS_SUCCESS); + when(dbQuery.getuploadStatusForAttachment(any(), any(), any(), any())).thenReturn(cmisDocument); // This should work as readAttachment handles the parsing internally handlerSpy.readAttachment(readContext); @@ -2189,7 +2254,9 @@ public void testReadAttachment_ExceptionInService() throws IOException { .when(sdmService) .readDocument( anyString(), any(SDMCredentials.class), any(AttachmentReadEventContext.class)); - + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setUploadStatus(SDMConstants.UPLOAD_STATUS_SUCCESS); + when(dbQuery.getuploadStatusForAttachment(any(), any(), any(), any())).thenReturn(cmisDocument); ServiceException thrown = assertThrows( ServiceException.class, @@ -2211,6 +2278,9 @@ public void testReadAttachment_WithSinglePartContentId() throws IOException { SDMCredentials mockSdmCredentials = new SDMCredentials(); when(tokenHandler.getSDMCredentials()).thenReturn(mockSdmCredentials); + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setUploadStatus(SDMConstants.UPLOAD_STATUS_SUCCESS); + when(dbQuery.getuploadStatusForAttachment(any(), any(), any(), any())).thenReturn(cmisDocument); handlerSpy.readAttachment(readContext); @@ -2219,6 +2289,82 @@ public void testReadAttachment_WithSinglePartContentId() throws IOException { verify(readContext).setCompleted(); } + @Test + public void testReadAttachment_WithSinglePartContentId_NotSuccess() throws IOException { + // Test scenario with single part content ID (no colon separator) + AttachmentReadEventContext readContext = mock(AttachmentReadEventContext.class); + when(readContext.getContentId()).thenReturn("singleObjectId"); + when(readContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); + when(mockAuthInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(mockJwtTokenInfo); + when(mockJwtTokenInfo.getToken()).thenReturn("dummyToken"); + + SDMCredentials mockSdmCredentials = new SDMCredentials(); + when(tokenHandler.getSDMCredentials()).thenReturn(mockSdmCredentials); + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setUploadStatus(SDMConstants.UPLOAD_STATUS_VIRUS_DETECTED); + when(dbQuery.getuploadStatusForAttachment(any(), any(), any(), any())).thenReturn(cmisDocument); + + ServiceException thrown = + assertThrows( + ServiceException.class, + () -> { + handlerSpy.readAttachment(readContext); + }); + // Should fail on draft entity not found, not on virus scan + assertEquals("Virus detected in this file kindly delete it.", thrown.getMessage()); + } + + @Test + public void testReadAttachment_WithSinglePartContentId_Uploading() throws IOException { + // Test scenario with single part content ID (no colon separator) + AttachmentReadEventContext readContext = mock(AttachmentReadEventContext.class); + when(readContext.getContentId()).thenReturn("singleObjectId"); + when(readContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); + when(mockAuthInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(mockJwtTokenInfo); + when(mockJwtTokenInfo.getToken()).thenReturn("dummyToken"); + + SDMCredentials mockSdmCredentials = new SDMCredentials(); + when(tokenHandler.getSDMCredentials()).thenReturn(mockSdmCredentials); + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setUploadStatus(SDMConstants.UPLOAD_STATUS_IN_PROGRESS); + when(dbQuery.getuploadStatusForAttachment(any(), any(), any(), any())).thenReturn(cmisDocument); + + ServiceException thrown = + assertThrows( + ServiceException.class, + () -> { + handlerSpy.readAttachment(readContext); + }); + // Should fail on draft entity not found, not on virus scan + assertEquals("UPLOAD_IN_PROGRESS_FILE_ERROR", thrown.getMessage()); + } + + @Test + public void testReadAttachment_WithSinglePartContentId_ScanInProgress() throws IOException { + // Test scenario with single part content ID (no colon separator) + AttachmentReadEventContext readContext = mock(AttachmentReadEventContext.class); + when(readContext.getContentId()).thenReturn("singleObjectId"); + when(readContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); + when(mockAuthInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(mockJwtTokenInfo); + when(mockJwtTokenInfo.getToken()).thenReturn("dummyToken"); + + SDMCredentials mockSdmCredentials = new SDMCredentials(); + when(tokenHandler.getSDMCredentials()).thenReturn(mockSdmCredentials); + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setUploadStatus(SDMConstants.VIRUS_SCAN_INPROGRESS); + when(dbQuery.getuploadStatusForAttachment(any(), any(), any(), any())).thenReturn(cmisDocument); + + ServiceException thrown = + assertThrows( + ServiceException.class, + () -> { + handlerSpy.readAttachment(readContext); + }); + // Should fail on draft entity not found, not on virus scan + assertEquals( + "Virus scanning is in progress. Refresh the page to see the effect.", thrown.getMessage()); + } + @Test public void testMarkAttachmentAsDeleted_MultipleObjectsInFolder() throws IOException { // Test scenario where multiple attachments exist and target object is among them @@ -2254,6 +2400,7 @@ public void testCreateAttachment_LargeFileVirusScanDisabled() throws IOException RepoValue repoValue = new RepoValue(); repoValue.setVirusScanEnabled(false); // Virus scan disabled repoValue.setVersionEnabled(false); + repoValue.setIsAsyncVirusScanEnabled(false); when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn(repoValue); // Set large file size (600MB) diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMCustomServiceHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMCustomServiceHandlerTest.java index 7b198e030..db6ef2d85 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMCustomServiceHandlerTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMCustomServiceHandlerTest.java @@ -38,6 +38,7 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Stream; +import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -382,8 +383,10 @@ void testMoveAttachments_SuccessWithoutSourceCleanup() throws IOException { + "\"}}"); // Mock getObject for metadata (no sourceFacet, so fetch from SDM) - when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test doc")); + JSONObject mockObjectResponse = new JSONObject(); + mockObjectResponse.put("cmis:name", "document.pdf"); + mockObjectResponse.put("cmis:description", "Test doc"); + when(sdmService.getObject(any(), any(), anyBoolean())).thenReturn(mockObjectResponse); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -460,8 +463,11 @@ void testMoveAttachments_ValidationFailure_InvalidSecondaryProperties() throws I + "\", \"invalidProp\": \"someValue\"}}"); // Mock getObject for metadata - when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test doc", "invalidProp")); + JSONObject mockObjectResponse2 = new JSONObject(); + mockObjectResponse2.put("cmis:name", "document.pdf"); + mockObjectResponse2.put("cmis:description", "Test doc"); + mockObjectResponse2.put("invalidProp", "someValue"); + when(sdmService.getObject(any(), any(), anyBoolean())).thenReturn(mockObjectResponse2); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -495,7 +501,8 @@ void testMoveAttachments_CreateDraftFailure_TriggersRollback() throws IOExceptio // Mock getObject when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "document.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -530,7 +537,8 @@ void testMoveAttachments_TargetFolderDoesNotExist_CreatesFolder() throws IOExcep // Mock getObject when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "document.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -564,7 +572,8 @@ void testMoveAttachments_PartialFailure_SomeSucceedSomeFail() throws IOException .thenThrow(new RuntimeException("Move failed for obj2")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "document.pdf").put("cmis:description", "Test doc")); // Execute sdmCustomServiceHandler.moveAttachments(context); @@ -695,7 +704,8 @@ void testMoveAttachments_ParseSDMErrorMessage_DuplicateError() throws IOExceptio "nameConstraintViolation : Child doc.pdf with Id xyz already exists")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -717,7 +727,8 @@ void testMoveAttachments_ParseSDMErrorMessage_VirusDetected() throws IOException .thenThrow(new RuntimeException("Virus scan status: cmis:virusScanStatus infected")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -738,7 +749,8 @@ void testMoveAttachments_ParseSDMErrorMessage_MalwareDetected() throws IOExcepti .thenThrow(new RuntimeException("Malware detected in file")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -759,7 +771,8 @@ void testMoveAttachments_ParseSDMErrorMessage_Unauthorized() throws IOException .thenThrow(new RuntimeException("User not authorized to perform this operation")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -780,7 +793,8 @@ void testMoveAttachments_ParseSDMErrorMessage_PermissionDenied() throws IOExcept .thenThrow(new RuntimeException("Permission denied for this resource")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -801,7 +815,8 @@ void testMoveAttachments_ParseSDMErrorMessage_BlockedMimeType() throws IOExcepti .thenThrow(new RuntimeException("MimeType application/exe is blocked")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -822,7 +837,8 @@ void testMoveAttachments_ParseSDMErrorMessage_FileNotFound() throws IOException .thenThrow(new RuntimeException("Object not found in repository")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -843,7 +859,8 @@ void testMoveAttachments_ParseSDMErrorMessage_GenericWithDetailedMessage() throw .thenThrow(new RuntimeException("SDM Error : Detailed error information here")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -864,7 +881,8 @@ void testMoveAttachments_ParseSDMErrorMessage_EmptyMessage() throws IOException .thenThrow(new RuntimeException("")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -886,7 +904,8 @@ void testMoveAttachments_ParseSDMErrorMessage_NullMessage() throws IOException { .thenThrow(nullMessageException); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -910,7 +929,8 @@ void testMoveAttachments_ExtractErrorMessage_WithCauseChain() throws IOException .thenThrow(genericCause); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -933,7 +953,8 @@ void testMoveAttachments_ParseDuplicateError_WithFilename() throws IOException { "nameConstraintViolation : Child document.pdf with Id abc123 already exists in folder")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "document.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -954,7 +975,8 @@ void testMoveAttachments_ParseDuplicateError_WithoutFilename() throws IOExceptio .thenThrow(new RuntimeException("Duplicate file constraint violation")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -975,7 +997,8 @@ void testMoveAttachments_ParseDuplicateError_NoColonSeparator() throws IOExcepti .thenThrow(new RuntimeException("duplicate")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -1132,7 +1155,11 @@ void testMoveAttachments_HandleValidationFailure_RollbackSuccess() throws IOExce + "\", \"invalidProp\": \"value\"}}"); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc", "invalidProp")); + .thenReturn( + new JSONObject() + .put("cmis:name", "doc.pdf") + .put("cmis:description", "Test doc") + .put("invalidProp", "someValue")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -1171,7 +1198,11 @@ void testMoveAttachments_HandleValidationFailure_RollbackFails() throws IOExcept .thenThrow(new RuntimeException("Rollback failed")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc", "invalidProp")); + .thenReturn( + new JSONObject() + .put("cmis:name", "doc.pdf") + .put("cmis:description", "Test doc") + .put("invalidProp", "someValue")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -1511,7 +1542,11 @@ void testProcessSecondaryProperty_NullValue() throws IOException { .thenReturn(List.of("nullProp")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc", "nullProp")); + .thenReturn( + new JSONObject() + .put("cmis:name", "doc.pdf") + .put("cmis:description", "Test doc") + .put("nullProp", (Object) null)); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -1546,7 +1581,11 @@ void testConvertValueIfNeeded_StringValue() throws IOException { .thenReturn(List.of("description")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc", "description")); + .thenReturn( + new JSONObject() + .put("cmis:name", "doc.pdf") + .put("cmis:description", "Test doc") + .put("description", "someValue")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -1581,7 +1620,11 @@ void testConvertValueIfNeeded_IntegerValue() throws IOException { .thenReturn(List.of("pageCount")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc", "pageCount")); + .thenReturn( + new JSONObject() + .put("cmis:name", "doc.pdf") + .put("cmis:description", "Test doc") + .put("pageCount", 10)); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -1618,7 +1661,11 @@ void testIsDateTimeField_NullElement() throws IOException { .thenReturn(List.of("unknownProp")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc", "unknownProp")); + .thenReturn( + new JSONObject() + .put("cmis:name", "doc.pdf") + .put("cmis:description", "Test doc") + .put("unknownProp", "someValue")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -1654,7 +1701,11 @@ void testRollbackSingleAttachment_Success() throws IOException { .thenReturn("{\"succinctProperties\": {\"cmis:objectId\": \"" + OBJECT_ID + "\"}}"); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc", "invalidProp")); + .thenReturn( + new JSONObject() + .put("cmis:name", "doc.pdf") + .put("cmis:description", "Test doc") + .put("invalidProp", "someValue")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -1692,7 +1743,11 @@ void testHandleValidationFailure_SingleInvalidProperty() throws IOException { .thenReturn("{\"succinctProperties\": {\"cmis:objectId\": \"" + OBJECT_ID + "\"}}"); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc", "invalidProp1")); + .thenReturn( + new JSONObject() + .put("cmis:name", "doc.pdf") + .put("cmis:description", "Test doc") + .put("invalidProp1", "someValue")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -1735,7 +1790,8 @@ void testHandleValidationFailure_MultipleInvalidProperties() throws IOException .thenReturn("{\"succinctProperties\": {\"cmis:objectId\": \"" + OBJECT_ID + "\"}}"); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -1775,7 +1831,8 @@ void testHandleValidationFailure_RollbackIOException() throws IOException { .thenThrow(new IOException("Network error during rollback")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -1815,7 +1872,8 @@ void testHandleValidationFailure_RollbackServiceException() throws IOException { .thenThrow(new RuntimeException("403 Forbidden - Access denied")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -1855,7 +1913,8 @@ void testHandleValidationFailure_FailureAddedToList() throws IOException { .thenReturn("{\"succinctProperties\": {\"cmis:objectId\": \"" + OBJECT_ID + "\"}}"); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -1897,7 +1956,8 @@ void testHandleValidationFailure_PreservesObjectId() throws IOException { .thenReturn("{\"succinctProperties\": {\"cmis:objectId\": \"" + specificObjectId + "\"}}"); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("doc.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "doc.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -1924,7 +1984,8 @@ void testCheckMaxCount_TargetEntityNotFound() throws IOException { + OBJECT_ID + "\"}}"); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "document.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -1951,7 +2012,8 @@ void testCheckMaxCount_MaxCountZero_NoLimitEnforced() throws IOException { + OBJECT_ID + "\"}}"); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "document.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -1989,7 +2051,8 @@ void testCheckMaxCount_DraftEntityNotFound_SkipValidation() throws IOException { + OBJECT_ID + "\"}}"); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "document.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2128,7 +2191,8 @@ void testCheckMaxCount_WithinLimit_ProceedWithMove() throws IOException { + OBJECT_ID + "\"}}"); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "document.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2180,7 +2244,8 @@ void testCheckMaxCount_ExceptionDuringValidation_ProceedWithMove() throws IOExce + OBJECT_ID + "\"}}"); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test doc")); + .thenReturn( + new JSONObject().put("cmis:name", "document.pdf").put("cmis:description", "Test doc")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2543,7 +2608,10 @@ void testMatchSpecificErrorType_DuplicateError_ReturnsParsedDuplicateMessage() .thenThrow(new ServiceException("duplicate file detected")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test document")); + .thenReturn( + new JSONObject() + .put("cmis:name", "document.pdf") + .put("cmis:description", "Test document")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2569,7 +2637,10 @@ void testMatchSpecificErrorType_NameConstraintViolation_ReturnsParsedDuplicateMe .thenThrow(new ServiceException("nameconstraintviolation occurred")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test document")); + .thenReturn( + new JSONObject() + .put("cmis:name", "document.pdf") + .put("cmis:description", "Test document")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2595,7 +2666,10 @@ void testMatchSpecificErrorType_VirusError_ReturnsMalwareMessage() throws IOExce .thenThrow(new ServiceException("virus detected in file")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test document")); + .thenReturn( + new JSONObject() + .put("cmis:name", "document.pdf") + .put("cmis:description", "Test document")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2622,7 +2696,10 @@ void testMatchSpecificErrorType_MalwareError_ReturnsMalwareMessage() throws IOEx .thenThrow(new ServiceException("malware found")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test document")); + .thenReturn( + new JSONObject() + .put("cmis:name", "document.pdf") + .put("cmis:description", "Test document")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2650,7 +2727,10 @@ void testMatchSpecificErrorType_UnauthorizedError_ReturnsAuthorizationMessage() .thenThrow(new ServiceException("unauthorized access")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test document")); + .thenReturn( + new JSONObject() + .put("cmis:name", "document.pdf") + .put("cmis:description", "Test document")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2678,7 +2758,10 @@ void testMatchSpecificErrorType_NotAuthorizedError_ReturnsAuthorizationMessage() .thenThrow(new ServiceException("user not authorized")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test document")); + .thenReturn( + new JSONObject() + .put("cmis:name", "document.pdf") + .put("cmis:description", "Test document")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2705,7 +2788,10 @@ void testMatchSpecificErrorType_PermissionError_ReturnsAuthorizationMessage() th .thenThrow(new ServiceException("permission denied")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test document")); + .thenReturn( + new JSONObject() + .put("cmis:name", "document.pdf") + .put("cmis:description", "Test document")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2731,7 +2817,10 @@ void testMatchSpecificErrorType_BlockedError_ReturnsMimeTypeMessage() throws IOE .thenThrow(new ServiceException("file blocked")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test document")); + .thenReturn( + new JSONObject() + .put("cmis:name", "document.pdf") + .put("cmis:description", "Test document")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2757,7 +2846,10 @@ void testMatchSpecificErrorType_MimeTypeError_ReturnsMimeTypeMessage() throws IO .thenThrow(new ServiceException("mimetype not allowed")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test document")); + .thenReturn( + new JSONObject() + .put("cmis:name", "document.pdf") + .put("cmis:description", "Test document")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2784,7 +2876,10 @@ void testMatchSpecificErrorType_NotFoundError_ReturnsFileNotFoundMessage() throw .thenThrow(new ServiceException("file not found")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test document")); + .thenReturn( + new JSONObject() + .put("cmis:name", "document.pdf") + .put("cmis:description", "Test document")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2812,7 +2907,10 @@ void testMatchSpecificErrorType_ObjectNotFoundError_ReturnsFileNotFoundMessage() .thenThrow(new ServiceException("object not found in repository")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test document")); + .thenReturn( + new JSONObject() + .put("cmis:name", "document.pdf") + .put("cmis:description", "Test document")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2839,7 +2937,10 @@ void testMatchSpecificErrorType_UnmatchedError_ReturnsNull() throws IOException .thenThrow(new ServiceException("network timeout error")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test document")); + .thenReturn( + new JSONObject() + .put("cmis:name", "document.pdf") + .put("cmis:description", "Test document")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2866,7 +2967,10 @@ void testParseDuplicateError_NoColon_ReturnsDefaultMessage() throws IOException .thenThrow(new ServiceException("duplicate file error")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test document")); + .thenReturn( + new JSONObject() + .put("cmis:name", "document.pdf") + .put("cmis:description", "Test document")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2895,7 +2999,10 @@ void testParseDuplicateError_ChildFormatWithFilename_ReturnsFormattedMessage() "nameConstraintViolation : Child document.pdf with Id abc123 already exists")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test document")); + .thenReturn( + new JSONObject() + .put("cmis:name", "document.pdf") + .put("cmis:description", "Test document")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2922,7 +3029,10 @@ void testParseDuplicateError_DetailedMessageWithoutChildFormat_ReturnsDetailedMe new ServiceException("duplicate : A file with the same name already exists in folder")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test document")); + .thenReturn( + new JSONObject() + .put("cmis:name", "document.pdf") + .put("cmis:description", "Test document")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2949,7 +3059,10 @@ void testParseDuplicateError_ChildFormatWithoutWithId_ReturnsDetailedMessage() .thenThrow(new ServiceException("duplicate : Child element already exists in target")); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("document.pdf", "Test document")); + .thenReturn( + new JSONObject() + .put("cmis:name", "document.pdf") + .put("cmis:description", "Test document")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -2982,7 +3095,8 @@ void testFetchAndSetLinkUrl_SuccessfullyFetchesAndSetsUrl() throws Exception { .thenReturn(sdmResponse); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("test.url", "Test link")); + .thenReturn( + new JSONObject().put("cmis:name", "test.url").put("cmis:description", "Test link")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -3009,7 +3123,8 @@ void testFetchAndSetLinkUrl_NullUrlReturned_ContinuesWithoutError() throws Excep .thenReturn(sdmResponse); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("test.url", "Test link")); + .thenReturn( + new JSONObject().put("cmis:name", "test.url").put("cmis:description", "Test link")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -3039,7 +3154,8 @@ void testFetchAndSetLinkUrl_ExceptionThrown_ContinuesWithWarning() throws Except .thenReturn(sdmResponse); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("test.url", "Test link")); + .thenReturn( + new JSONObject().put("cmis:name", "test.url").put("cmis:description", "Test link")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -3069,7 +3185,8 @@ void testFetchAndSetLinkUrl_ServiceExceptionThrown_ContinuesWithWarning() throws .thenReturn(sdmResponse); when(sdmService.getObject(any(), any(), anyBoolean())) - .thenReturn(List.of("test.url", "Test link")); + .thenReturn( + new JSONObject().put("cmis:name", "test.url").put("cmis:description", "Test link")); AttachmentMoveEventContext context = createMockMoveContext(false); @@ -3096,7 +3213,8 @@ void testProcessSecondaryProperty_WithValidValue_AddsToFilteredMap() throws Exce when(sdmService.moveAttachment(any(CmisDocument.class), any(), anyBoolean())) .thenReturn(sdmResponse); - when(sdmService.getObject(any(), any(), anyBoolean())).thenReturn(List.of("test.pdf", "Test")); + when(sdmService.getObject(any(), any(), anyBoolean())) + .thenReturn(new JSONObject().put("cmis:name", "test.pdf").put("cmis:description", "Test")); AttachmentMoveEventContext context = createMockMoveContext(false); sdmCustomServiceHandler.moveAttachments(context); @@ -3119,7 +3237,8 @@ void testProcessSecondaryProperty_WithNullValue_SkipsProperty() throws Exception when(sdmService.moveAttachment(any(CmisDocument.class), any(), anyBoolean())) .thenReturn(sdmResponse); - when(sdmService.getObject(any(), any(), anyBoolean())).thenReturn(List.of("test.pdf", "Test")); + when(sdmService.getObject(any(), any(), anyBoolean())) + .thenReturn(new JSONObject().put("cmis:name", "test.pdf").put("cmis:description", "Test")); AttachmentMoveEventContext context = createMockMoveContext(false); sdmCustomServiceHandler.moveAttachments(context); @@ -3141,7 +3260,8 @@ void testProcessSecondaryProperty_WithJSONNull_SkipsProperty() throws Exception when(sdmService.moveAttachment(any(CmisDocument.class), any(), anyBoolean())) .thenReturn(sdmResponse); - when(sdmService.getObject(any(), any(), anyBoolean())).thenReturn(List.of("test.pdf", "Test")); + when(sdmService.getObject(any(), any(), anyBoolean())) + .thenReturn(new JSONObject().put("cmis:name", "test.pdf").put("cmis:description", "Test")); AttachmentMoveEventContext context = createMockMoveContext(false); sdmCustomServiceHandler.moveAttachments(context); @@ -3166,7 +3286,8 @@ void testConvertValueIfNeeded_LongToInstantForDateTime_ConvertsSuccessfully() th when(sdmService.moveAttachment(any(CmisDocument.class), any(), anyBoolean())) .thenReturn(sdmResponse); - when(sdmService.getObject(any(), any(), anyBoolean())).thenReturn(List.of("test.pdf", "Test")); + when(sdmService.getObject(any(), any(), anyBoolean())) + .thenReturn(new JSONObject().put("cmis:name", "test.pdf").put("cmis:description", "Test")); AttachmentMoveEventContext context = createMockMoveContext(false); sdmCustomServiceHandler.moveAttachments(context); @@ -3187,7 +3308,8 @@ void testConvertValueIfNeeded_LongForNonDateTime_KeepsAsLong() throws Exception when(sdmService.moveAttachment(any(CmisDocument.class), any(), anyBoolean())) .thenReturn(sdmResponse); - when(sdmService.getObject(any(), any(), anyBoolean())).thenReturn(List.of("test.pdf", "Test")); + when(sdmService.getObject(any(), any(), anyBoolean())) + .thenReturn(new JSONObject().put("cmis:name", "test.pdf").put("cmis:description", "Test")); AttachmentMoveEventContext context = createMockMoveContext(false); sdmCustomServiceHandler.moveAttachments(context); @@ -3208,7 +3330,8 @@ void testConvertValueIfNeeded_NonLongValue_ReturnsOriginal() throws Exception { when(sdmService.moveAttachment(any(CmisDocument.class), any(), anyBoolean())) .thenReturn(sdmResponse); - when(sdmService.getObject(any(), any(), anyBoolean())).thenReturn(List.of("test.pdf", "Test")); + when(sdmService.getObject(any(), any(), anyBoolean())) + .thenReturn(new JSONObject().put("cmis:name", "test.pdf").put("cmis:description", "Test")); AttachmentMoveEventContext context = createMockMoveContext(false); sdmCustomServiceHandler.moveAttachments(context); @@ -3232,7 +3355,8 @@ void testIsDateTimeField_WithDateTimeElement_ReturnsTrue() throws Exception { when(sdmService.moveAttachment(any(CmisDocument.class), any(), anyBoolean())) .thenReturn(sdmResponse); - when(sdmService.getObject(any(), any(), anyBoolean())).thenReturn(List.of("test.pdf", "Test")); + when(sdmService.getObject(any(), any(), anyBoolean())) + .thenReturn(new JSONObject().put("cmis:name", "test.pdf").put("cmis:description", "Test")); AttachmentMoveEventContext context = createMockMoveContext(false); sdmCustomServiceHandler.moveAttachments(context); @@ -3253,7 +3377,8 @@ void testIsDateTimeField_WithNonDateTimeElement_ReturnsFalse() throws Exception when(sdmService.moveAttachment(any(CmisDocument.class), any(), anyBoolean())) .thenReturn(sdmResponse); - when(sdmService.getObject(any(), any(), anyBoolean())).thenReturn(List.of("test.pdf", "Test")); + when(sdmService.getObject(any(), any(), anyBoolean())) + .thenReturn(new JSONObject().put("cmis:name", "test.pdf").put("cmis:description", "Test")); AttachmentMoveEventContext context = createMockMoveContext(false); sdmCustomServiceHandler.moveAttachments(context); diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java index 8bdfe414a..9e1e31ae0 100644 --- a/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMServiceGenericHandlerTest.java @@ -479,14 +479,17 @@ void testCreate_shouldCreateLink() throws IOException { createResult.put("objectId", "obj123"); createResult.put("folderId", "folderId123"); createResult.put("message", "ok"); - when(documentService.createDocument(any(), any(), anyBoolean())).thenReturn(createResult); + when(documentService.createDocument( + any(CmisDocument.class), any(SDMCredentials.class), anyBoolean(), any())) + .thenReturn(createResult); // Act sdmServiceGenericHandler.create(mockContext); // Assert verify(sdmService).checkRepositoryType(anyString(), anyString()); - verify(documentService).createDocument(any(), any(), anyBoolean()); + verify(documentService) + .createDocument(any(CmisDocument.class), any(SDMCredentials.class), anyBoolean(), any()); verify(draftService).newDraft(any(Insert.class)); verify(mockContext).setCompleted(); } @@ -870,7 +873,8 @@ void testCreate_ThrowsServiceException_WhenCreateDocumentThrowsException() throw sdmCredentials.setUrl("http://test-url"); when(tokenHandler.getSDMCredentials()).thenReturn(sdmCredentials); - when(documentService.createDocument(any(), any(), anyBoolean())) + when(documentService.createDocument( + any(CmisDocument.class), any(SDMCredentials.class), anyBoolean(), any())) .thenThrow(new RuntimeException("Document creation failed")); // Act & Assert @@ -957,7 +961,9 @@ void testCreate_ThrowsServiceExceptionOnDuplicateStatus() throws IOException { createResult.put("objectId", "obj123"); createResult.put("folderId", "folderId123"); createResult.put("message", "Duplicate file"); - when(documentService.createDocument(any(), any(), anyBoolean())).thenReturn(createResult); + when(documentService.createDocument( + any(CmisDocument.class), any(SDMCredentials.class), anyBoolean(), any())) + .thenReturn(createResult); // Act & Assert ServiceException ex = @@ -1039,7 +1045,9 @@ void testCreate_ThrowsServiceExceptionOnFailStatus() throws IOException { createResult.put("objectId", "obj123"); createResult.put("folderId", "folderId123"); createResult.put("message", "Some error message"); - when(documentService.createDocument(any(), any(), anyBoolean())).thenReturn(createResult); + when(documentService.createDocument( + any(CmisDocument.class), any(SDMCredentials.class), anyBoolean(), any())) + .thenReturn(createResult); // Act & Assert ServiceException ex = @@ -1123,7 +1131,9 @@ void testCreate_ThrowsServiceExceptionOnUnauthorizedStatus() throws IOException createResult.put("objectId", "obj123"); createResult.put("folderId", "folderId123"); createResult.put("message", "Unauthorized"); - when(documentService.createDocument(any(), any(), anyBoolean())).thenReturn(createResult); + when(documentService.createDocument( + any(CmisDocument.class), any(SDMCredentials.class), anyBoolean(), any())) + .thenReturn(createResult); // Act & Assert ServiceException ex = @@ -1137,6 +1147,9 @@ void testCreate_ThrowsServiceExceptionOnUnauthorizedStatus() throws IOException void testOpenAttachment_InternetShortcut() throws Exception { // Arrange AttachmentReadContext context = mock(AttachmentReadContext.class); + UserInfo userInfo = mock(UserInfo.class); + when(userInfo.isSystemUser()).thenReturn(false); + when(context.getUserInfo()).thenReturn(userInfo); CdsModel cdsModel = mock(CdsModel.class); CdsEntity cdsEntity = mock(CdsEntity.class); CqnSelect cqnSelect = mock(CqnSelect.class); @@ -1161,10 +1174,18 @@ void testOpenAttachment_InternetShortcut() throws Exception { cmisDocument.setFileName("file.url"); cmisDocument.setMimeType("application/internet-shortcut"); cmisDocument.setUrl("http://shortcut-url"); + cmisDocument.setObjectId("object-123"); when(dbQuery.getObjectIdForAttachmentID(cdsEntity, persistenceService, "123")) .thenReturn(cmisDocument); + // Mock token handler and SDM service object check to pass + SDMCredentials creds = new SDMCredentials(); + when(tokenHandler.getSDMCredentials()).thenReturn(creds); + JSONObject objResp = new JSONObject(); + objResp.put("status", "success"); + when(sdmService.getObject(eq("object-123"), eq(creds), eq(false))).thenReturn(objResp); + // Act sdmServiceGenericHandler.openAttachment(context); @@ -1340,6 +1361,9 @@ void testEditLinkFailure() throws IOException { void testOpenAttachment_WithLinkFile() throws Exception { // Arrange AttachmentReadContext context = mock(AttachmentReadContext.class); + UserInfo userInfo = mock(UserInfo.class); + when(userInfo.isSystemUser()).thenReturn(false); + when(context.getUserInfo()).thenReturn(userInfo); when(context.getModel()).thenReturn(cdsModel); when(context.getTarget()).thenReturn(cdsEntity); when(cdsEntity.getQualifiedName()).thenReturn("MyEntity"); @@ -1356,9 +1380,19 @@ void testOpenAttachment_WithLinkFile() throws Exception { linkDocument.setFileName("test.url"); linkDocument.setMimeType("application/internet-shortcut"); linkDocument.setUrl("http://test.com"); + linkDocument.setObjectId("object123"); when(dbQuery.getObjectIdForAttachmentID(eq(draftEntity), eq(persistenceService), eq("123"))) .thenReturn(linkDocument); + // Mock token handler and SDM service for internet shortcut verification + SDMCredentials sdmCredentials = new SDMCredentials(); + when(tokenHandler.getSDMCredentials()).thenReturn(sdmCredentials); + + JSONObject objectResponse = new JSONObject(); + objectResponse.put("status", "success"); + when(sdmService.getObject(eq("object123"), eq(sdmCredentials), eq(false))) + .thenReturn(objectResponse); + // Act sdmServiceGenericHandler.openAttachment(context); @@ -1399,6 +1433,9 @@ void testOpenAttachment_WithRegularFile() throws Exception { void testOpenAttachment_FallbackToNonDraftEntity() throws Exception { // Arrange AttachmentReadContext context = mock(AttachmentReadContext.class); + UserInfo userInfo = mock(UserInfo.class); + when(userInfo.isSystemUser()).thenReturn(false); + when(context.getUserInfo()).thenReturn(userInfo); when(context.getModel()).thenReturn(cdsModel); when(context.getTarget()).thenReturn(cdsEntity); when(cdsEntity.getQualifiedName()).thenReturn("MyEntity"); @@ -1423,9 +1460,17 @@ void testOpenAttachment_FallbackToNonDraftEntity() throws Exception { properDocument.setFileName("test.url"); properDocument.setMimeType("application/internet-shortcut"); properDocument.setUrl("http://fallback.com"); + properDocument.setObjectId("object-456"); when(dbQuery.getObjectIdForAttachmentID(eq(cdsEntity), eq(persistenceService), eq("123"))) .thenReturn(properDocument); + // Mock token handler and SDM service object check to pass + SDMCredentials creds = new SDMCredentials(); + when(tokenHandler.getSDMCredentials()).thenReturn(creds); + JSONObject objResp = new JSONObject(); + objResp.put("status", "success"); + when(sdmService.getObject(eq("object-456"), eq(creds), eq(false))).thenReturn(objResp); + // Act sdmServiceGenericHandler.openAttachment(context); @@ -1512,6 +1557,8 @@ void testCreateLink_AttachmentCountConstraintExceeded() throws IOException { when(analysisResult.rootKeys()).thenReturn(Map.of("ID", "123")); when(draftEntity.findAssociation("up_")).thenReturn(Optional.of(mockAssociationElement)); when(mockAssociationElement.getType()).thenReturn(mockAssociationType); + when(mockAssociationType.getTarget()).thenReturn(cdsEntity); + when(cdsModel.findEntity("MyService.MyEntity")).thenReturn(Optional.of(cdsEntity)); when(mockAssociationType.refs()).thenReturn(Stream.of(mockCqnElementRef)); when(mockCqnElementRef.path()).thenReturn("ID"); @@ -1553,6 +1600,7 @@ void testCreateLink_RestrictedCharactersInName() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments_drafts")) .thenReturn(Optional.of(draftEntity)); when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); + when(cdsModel.findEntity("MyService.MyEntity")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); when(cqnSelect.toString()) @@ -1576,6 +1624,7 @@ void testCreateLink_RestrictedCharactersInName() throws IOException { when(analysisResult.rootKeys()).thenReturn(Map.of("ID", "123")); when(draftEntity.findAssociation("up_")).thenReturn(Optional.of(mockAssociationElement)); when(mockAssociationElement.getType()).thenReturn(mockAssociationType); + when(mockAssociationType.getTarget()).thenReturn(cdsEntity); when(mockAssociationType.refs()).thenReturn(Stream.of(mockCqnElementRef)); when(mockCqnElementRef.path()).thenReturn("ID"); @@ -1614,6 +1663,7 @@ void testCreateLink_UnauthorizedError() throws IOException { when(cdsModel.findEntity("MyService.MyEntity.attachments_drafts")) .thenReturn(Optional.of(draftEntity)); when(cdsModel.findEntity("MyService.MyEntity.attachments")).thenReturn(Optional.of(cdsEntity)); + when(cdsModel.findEntity("MyService.MyEntity")).thenReturn(Optional.of(cdsEntity)); when(mockContext.getEvent()).thenReturn("createLink"); CqnSelect cqnSelect = mock(CqnSelect.class); when(cqnSelect.toString()) @@ -1664,7 +1714,7 @@ void testCreateLink_UnauthorizedError() throws IOException { JSONObject createResult = new JSONObject(); createResult.put("status", "unauthorized"); when(documentService.createDocument( - any(CmisDocument.class), any(SDMCredentials.class), anyBoolean())) + any(CmisDocument.class), any(SDMCredentials.class), anyBoolean(), any())) .thenReturn(createResult); // Act & Assert