diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml new file mode 100644 index 00000000..c8186029 --- /dev/null +++ b/.github/actions/build/action.yml @@ -0,0 +1,39 @@ +name: Maven Build +description: "Builds a Maven project." + +inputs: + java-version: + description: "The Java version the build shall run with." + required: true + maven-version: + description: "The Maven version the build shall run with." + required: true + mutation-testing: + description: "Whether to run mutation testing." + default: 'true' + required: false + +runs: + using: composite + steps: + - name: Set up Java ${{ inputs.java-version }} + uses: actions/setup-java@v4 + with: + java-version: ${{ inputs.java-version }} + distribution: sapmachine + cache: maven + + - name: Setup Maven ${{ inputs.maven-version }} + uses: stCarolas/setup-maven@v5 + with: + maven-version: ${{ inputs.maven-version }} + + - name: Build with Maven + run: | + mvn clean install -P unit-tests -DskipIntegrationTests + shell: bash + + # - name: Piper Maven build + # uses: SAP/project-piper-action@main + # with: + # step-name: mavenBuild diff --git a/.github/actions/deploy-release/action.yml b/.github/actions/deploy-release/action.yml new file mode 100644 index 00000000..2daca2f8 --- /dev/null +++ b/.github/actions/deploy-release/action.yml @@ -0,0 +1,97 @@ +name: Maven Release +description: "Deploys a Maven package to Maven Central repository." + +inputs: + user: + description: "The user used for the upload (technical user for maven central upload)" + required: true + password: + description: "The password used for the upload (technical user for maven central upload)" + required: true + profile: + description: "The profile id" + required: true + pgp-pub-key: + description: "The public pgp key ID" + required: true + pgp-private-key: + description: "The private pgp key" + required: true + pgp-passphrase: + description: "The passphrase for pgp" + required: true + revision: + description: "The revision of sdm" + required: true + +runs: + using: composite + steps: + - name: "Echo Inputs" + run: | + echo "user: ${{ inputs.user }}" + echo "profile: ${{ inputs.profile }}" + shell: bash + + - name: "Setup Java" + uses: actions/setup-java@v4 + with: + distribution: 'sapmachine' + java-version: '17' + server-id: ossrh + server-username: MAVEN_CENTRAL_USER + server-password: MAVEN_CENTRAL_PASSWORD + + - name: "Import GPG Key" + run: | + echo "${{ inputs.pgp-private-key }}" | gpg --batch --passphrase "$PASSPHRASE" --import + shell: bash + env: + PASSPHRASE: ${{ inputs.pgp-passphrase }} + + - name: "Ensure Local Repo Directory" + run: | + mkdir -p ./deploy-oss/temp_local_repo + ls -al ./deploy-oss + shell: bash + + - name: "Deploy Locally" + run: | + echo "Deploying artifacts locally..." + mvn --batch-mode --no-transfer-progress --fail-at-end --show-version \ + -Durl=file:./temp_local_repo \ + -Dmaven.install.skip=true \ + -Dmaven.test.skip=true \ + -Dgpg.passphrase="$GPG_PASSPHRASE" \ + -Dgpg.keyname="$GPG_PUB_KEY" \ + -Drevision="${{ inputs.revision }}" \ + deploy + working-directory: ./deploy-oss + shell: bash + env: + MAVEN_CENTRAL_USER: ${{ inputs.user }} + MAVEN_CENTRAL_PASSWORD: ${{ inputs.password }} + GPG_PASSPHRASE: ${{ inputs.pgp-passphrase }} + GPG_PUB_KEY: ${{ inputs.pgp-pub-key }} + + - name: "List Contents of Local Repo" + run: | + echo "Contents of temp_local_repo:" + ls -al ./deploy-oss/temp_local_repo + shell: bash + + - name: "Deploy Staging" + run: | + mvn --batch-mode --no-transfer-progress --fail-at-end --show-version \ + org.sonatype.plugins:nexus-staging-maven-plugin:1.6.13:deploy-staged-repository \ + -DserverId=ossrh \ + -DnexusUrl=https://oss.sonatype.org \ + -DrepositoryDirectory=./temp_local_repo \ + -DstagingProfileId="$MAVEN_CENTRAL_PROFILE_ID" \ + -Drevision="${{ inputs.revision }}" + working-directory: ./deploy-oss + shell: bash + env: + MAVEN_CENTRAL_USER: ${{ inputs.user }} + MAVEN_CENTRAL_PASSWORD: ${{ inputs.password }} + MAVEN_CENTRAL_PROFILE_ID: ${{ inputs.profile }} diff --git a/.github/actions/deploy/action.yml b/.github/actions/deploy/action.yml new file mode 100644 index 00000000..027923cc --- /dev/null +++ b/.github/actions/deploy/action.yml @@ -0,0 +1,62 @@ +name: Deploy to artifactory +description: "Deploys artifacts to artifactory." + +inputs: + repository-url: + description: "The URL of the repository to upload to." + required: true + server-id: + description: "The service id of the repository to upload to." + required: true + user: + description: "The user used for the upload." + required: true + password: + description: "The password used for the upload." + required: true + pom-file: + description: "The path to the POM file." + required: false + default: "pom.xml" + maven-version: + description: "The Maven version the build shall run with." + required: true + +runs: + using: composite + steps: + - name: Echo Inputs + run: | + echo "repository-url: ${{ inputs.repository-url }}" + echo "user: ${{ inputs.user }}" + echo "password: ${{ inputs.password }}" + echo "pom-file: ${{ inputs.pom-file }}" + echo "altDeploymentRepository: ${{inputs.server-id}}::default::${{inputs.repository-url}}" + shell: bash + + - name: Setup Java 17 + uses: actions/setup-java@v4 + with: + distribution: sapmachine + java-version: '17' + server-id: ${{ inputs.server-id }} + server-username: CAP_DEPLOYMENT_USER + server-password: CAP_DEPLOYMENT_PASS + + - name: Setup Maven ${{ inputs.maven-version }} + uses: stCarolas/setup-maven@v5 + with: + maven-version: ${{ inputs.maven-version }} + + - name: Deploy + run: > + mvn -B -ntp --fae --show-version + -DaltDeploymentRepository=${{inputs.server-id}}::default::${{inputs.repository-url}} + -Dmaven.install.skip=true + -Dmaven.test.skip=true + -f ${{ inputs.pom-file }} + deploy + env: + CAP_DEPLOYMENT_USER: ${{ inputs.user }} + CAP_DEPLOYMENT_PASS: ${{ inputs.password }} + shell: bash diff --git a/.github/actions/newrelease/action.yml b/.github/actions/newrelease/action.yml new file mode 100644 index 00000000..b972f393 --- /dev/null +++ b/.github/actions/newrelease/action.yml @@ -0,0 +1,40 @@ +name: Update POM with new release +description: Updates the revision property in the POM file with the new release version. + +inputs: + java-version: + description: "The Java version the build shall run with." + required: true + maven-version: + description: "The Maven version the build shall run with." + default: '3.6.3' + required: false + +runs: + using: composite + steps: + - name: Set up JDK ${{ inputs.java-version }} + uses: actions/setup-java@v4 + with: + java-version: ${{ inputs.java-version }} + distribution: sapmachine + cache: maven + + - name: Setup Maven ${{ inputs.maven-version }} + uses: stCarolas/setup-maven@v5 + with: + maven-version: ${{ inputs.maven-version }} + + - name: Update version + run: | + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + echo $VERSION > cap-notebook/version.txt + mvn --no-transfer-progress versions:set-property -Dproperty=revision -DnewVersion=$VERSION + #chmod +x ensure-license.sh + #./ensure-license.sh + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + git checkout -b develop + git commit -am "Update version to $VERSION" + git push --set-upstream origin develop + shell: bash diff --git a/.github/workflows/blackduck.yml b/.github/workflows/blackduck.yml new file mode 100644 index 00000000..70cd6f20 --- /dev/null +++ b/.github/workflows/blackduck.yml @@ -0,0 +1,56 @@ +name: Blackduck analysis + +on: + push: + branches: + - develop + pull_request: + branches: + - develop + types: [opened, synchronize, reopened] + workflow_dispatch: + +permissions: + pull-requests: read # allows SonarQube to decorate PRs with analysis results + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + + - name: Install dependencies + run: | + mvn clean install -P unit-tests -DskipIntegrationTests + + - name: Download Synopsys Detect Script + run: curl --silent -O https://detect.synopsys.com/detect9.sh + + - name: Run & analyze BlackDuck Scan + run: | + bash ./detect9.sh -d \ + --logging.level.com.synopsys.integration=DEBUG \ + --blackduck.url="https://sap.blackducksoftware.com" \ + --blackduck.api.token=""${{ secrets.BLACKDUCK_TOKEN }}"" \ + --detect.blackduck.signature.scanner.arguments="--min-scan-interval=0" \ + --detect.maven.build.command="install -P unit-tests -DskipIntegrationTests" \ + --detect.latest.release.version="9.6.0" \ + --detect.project.version.distribution="SaaS" \ + --detect.blackduck.signature.scanner.memory=4096 \ + --detect.timeout=6000 \ + --blackduck.trust.cert=true \ + --detect.project.user.groups="SAP_DOC_MGMT_CAPPLUGIN_JAVA1.0" \ + --detect.project.name="SAP_DOC_MGMT_CAPPLUGIN_JAVA1.0" \ + --detect.project.version.name="1.0" \ + --detect.code.location.name="SAP_DOC_MGMT_CAPPLUGIN_JAVA1.0/1.0" \ + --detect.source.path="/home/runner/work/sdm/sdm/sdm" diff --git a/.github/workflows/cfdeploy.yml b/.github/workflows/cfdeploy.yml new file mode 100644 index 00000000..157a0d99 --- /dev/null +++ b/.github/workflows/cfdeploy.yml @@ -0,0 +1,84 @@ +name: Deploy + +on: + workflow_dispatch: + inputs: + cf_space: + description: 'Specify the Cloud Foundry space to deploy to' + required: true + repository_id: + description: 'Specify the Repository ID (leave blank if deploying to developcap)' + required: false + +permissions: + pull-requests: read + +jobs: + Deploy: + runs-on: cap-java + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + ref: develop + + - name: Set up Java 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'temurin' + + - name: Verify and Checkout Deploy Branch + run: | + git fetch origin + if git rev-parse --verify origin/develop_deploy; then + git checkout develop_deploy + else + echo "Branch 'develop_deploy' not found. Please verify the branch name." + exit 1 + fi + + - name: Deleting the sdm directory for fresh build + run: | + pwd + cd + rm -rf .m2/repository/com/sap/cds + + - name: Set REPOSITORY_ID + id: set_repository_id + run: | + if [ "${{ github.event.inputs.cf_space }}" = "developcap" ]; then + echo "Using REPOSITORY_ID from secrets' + echo "::set-output name=repository_id::${{ secrets.REPOSITORY_ID }}" + else + if [ -z "${{ github.event.inputs.repository_id }}" ]; then + echo "REPOSITORY_ID must be provided for non-developcap spaces" + exit 1 + else + echo "Using provided REPOSITORY_ID" + echo "::set-output name=repository_id::${{ github.event.inputs.repository_id }}" + fi + fi + + - name: Prepare and Deploy to Cloud Foundry + run: | + echo "Current Branch......" + git branch + pwd + cd /sapmnt/home/I355238/actions-runner/_work/sdm/sdm/cap-notebook/demoapp + + # Replace placeholder with actual REPOSITORY_ID value + sed -i 's|__REPOSITORY_ID__|'${{ steps.set_repository_id.outputs.repository_id }}'|g' ./mta.yaml + + mbt build + + # Install cf CLI plugin + cf install-plugin multiapps -f + + # Login to Cloud Foundry again to ensure session is active + cf login -a ${{ secrets.CF_API }} -u ${{ secrets.CF_USER }} -p ${{ secrets.CF_PASSWORD }} -o ${{ secrets.CF_ORG }} -s ${{ github.event.inputs.cf_space }} + + # Deploy the application + echo "Running cf deploy" + cf deploy mta_archives/demoappjava_1.0.0.mtar -f diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..f2ee6959 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,49 @@ +name: "CodeQL Analysis" + +on: + push: + branches: ["develop", "Release*"] + pull_request: + branches: ["develop", Release*"] + schedule: + - cron: '0 0 * * 0' # Runs every Sunday at midnight + + workflow_dispatch: + +jobs: + analyze: + name: Analyze + runs-on: cap-java + + permissions: + security-events: write # Needed for CodeQL to upload results to the Security tab + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + language: [java, java-kotlin] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: '17' # or '17' if your project uses JDK 17 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: '/language:{{ matrix.language }}' diff --git a/.github/workflows/deploy_and_Integration_test.yml b/.github/workflows/deploy_and_Integration_test.yml new file mode 100644 index 00000000..4160c033 --- /dev/null +++ b/.github/workflows/deploy_and_Integration_test.yml @@ -0,0 +1,162 @@ +name: Deploy and Integration Test + +on: + pull_request: + types: [closed] + branches: + - develop + workflow_dispatch: + +permissions: + pull-requests: read + +jobs: + deploy: + if: github.event.pull_request.merged == true + runs-on: cap-java + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + ref: develop + + - name: Set up Java 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'temurin' + + - name: Verify and Checkout Deploy Branch + run: | + git fetch origin + if git rev-parse --verify origin/develop_deploy; then + git checkout develop_deploy + else + echo "Branch 'develop_deploy' not found. Please verify the branch name." + exit 1 + fi + - name: Deleting the sdm directory for fresh build + run: | + pwd + cd + rm -rf .m2/repository/com/sap/cds + - name: Prepare and Deploy to Cloud Foundry + run: | + echo "Current Branch......" + git branch + pwd + cd /sapmnt/home/I355238/actions-runner/_work/sdm/sdm/cap-notebook/demoapp + + #Replace placeholder with actual REPOSITORY_ID value + sed -i 's|__REPOSITORY_ID__|'${{ secrets.REPOSITORY_ID }}'|g' ./mta.yaml + + mbt build + + # Install cf CLI plugin + cf install-plugin multiapps -f + + # Login to Cloud Foundry again to ensure session is active + cf login -a ${{ secrets.CF_API }} -u ${{ secrets.CF_USER }} -p ${{ secrets.CF_PASSWORD }} -o ${{ secrets.CF_ORG }} -s ${{ secrets.CF_SPACE }} + + # Deploy the application + echo "Running cf deploy" + cf deploy mta_archives/demoappjava_1.0.0.mtar -f + + integration-test: + needs: deploy + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Java 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'temurin' + + - name: Install Cloud Foundry CLI and jq + run: | + wget -q -O - https://packages.cloudfoundry.org/debian/cli.cloudfoundry.org.key | sudo apt-key add - + echo "deb https://packages.cloudfoundry.org/debian stable main" | sudo tee /etc/apt/sources.list.d/cloudfoundry-cli.list + sudo apt-get update + sudo apt-get install cf8-cli jq + - name: Login to Cloud Foundry + run: | + cf login -a ${{ secrets.CF_API }} \ + -u ${{ secrets.CF_USER }} \ + -p ${{ secrets.CF_PASSWORD }} \ + -o ${{ secrets.CF_ORG }} \ + -s ${{ secrets.CF_SPACE }} + - name: Fetch and Escape Client Secret + id: fetch_secret + run: | + # Fetch the service instance GUID + service_instance_guid=$(cf service demoappjava-public-uaa --guid) + if [ -z "$service_instance_guid" ]; then + echo "Error: Unable to retrieve service instance GUID"; exit 1; + fi + # Fetch the binding GUID + bindings_response=$(cf curl "/v3/service_credential_bindings?service_instance_guids=${service_instance_guid}") + + binding_guid=$(echo $bindings_response | jq -r '.resources[0].guid') + if [ -z "$binding_guid" ]; then + echo "Error: Unable to retrieve binding GUID"; exit 1; + fi + + # Fetch the clientSecret + binding_details=$(cf curl "/v3/service_credential_bindings/${binding_guid}/details") + clientSecret=$(echo "$binding_details" | jq -r '.credentials.clientsecret') + if [ -z "$clientSecret" ] || [ "$clientSecret" == "null" ]; then + echo "Error: clientSecret is not set or is null"; exit 1; + fi + + # Escape any $ characters in the clientSecret + escapedClientSecret=$(echo "$clientSecret" | sed 's/\$/\\$/g') + echo "::set-output name=CLIENT_SECRET::$escapedClientSecret" + - name: Run integration tests + env: + CLIENT_SECRET: ${{ steps.fetch_secret.outputs.CLIENT_SECRET }} + run: | + set -e # Enable error checking + PROPERTIES_FILE="sdm/src/test/resources/credentials.properties" + # Gather secrets and other values + appUrl="${{ secrets.CF_ORG }}-${{ secrets.CF_SPACE }}-demoappjava-srv.cfapps.eu12.hana.ondemand.com" + authUrl="${{ secrets.CAPAUTH_URL }}" + clientID="${{ secrets.CAPSDM_CLIENT_ID }}" + clientSecret="${{ env.CLIENT_SECRET }}" + username="${{ secrets.CF_USER }}" + password="${{ secrets.CF_PASSWORD }}" + # Ensure all required variables are set + if [ -z "$appUrl" ]; then echo "Error: appUrl is not set"; exit 1; fi + if [ -z "$authUrl" ]; then echo "Error: authUrl is not set"; exit 1; fi + if [ -z "$clientID" ]; then echo "Error: clientID is not set"; exit 1; fi + if [ -z "$clientSecret" ]; then echo "Error: clientSecret is not set"; exit 1; fi + if [ -z "$username" ]; then echo "Error: username is not set"; exit 1; fi + if [ -z "$password" ]; then echo "Error: password is not set"; exit 1; fi + # Function to partially mask sensitive information for logging + mask() { + local value="$1" + if [ ${#value} -gt 6 ]; then + echo "${value:0:3}*****${value: -3}" + else + echo "${value:0:2}*****" + fi + } + # Update properties file with real values + cat > "$PROPERTIES_FILE" < "$PROPERTIES_FILE" <> $GITHUB_ENV + shell: bash + + - name: Create Branch if Version is not SNAPSHOT + if: "! contains(env.REVISION, '-SNAPSHOT')" + id: create-branch + run: | + current_version=${{ env.REVISION }} + new_branch="Release_v${current_version}" + echo "Checking if branch $new_branch already exists" + if git ls-remote --exit-code --heads origin $new_branch; then + echo "Branch $new_branch already exists, not creating new branch." + else + echo "Branch $new_branch does not exist, creating new branch." + git config --local user.name "github-actions" + git config --local user.email "github-actions@github.com" + git checkout -b $new_branch + git push origin $new_branch + fi + shell: bash + + - name: Check and Update Version + id: check-and-update-version + if: contains(env.REVISION, '-SNAPSHOT') == false + run: | + current_version=${{ env.REVISION }} + # Check if the version already contains '-SNAPSHOT' + if [[ $current_version != *-SNAPSHOT ]]; then + echo "Current version does not contain -SNAPSHOT, updating version..." + # Split version into major, minor, and patch parts + IFS='.' read -r major minor patch <<< "$(echo $current_version | tr '-' '.')" + # Increment the patch number + new_patch=$((patch + 1)) + # Form the new version + new_version="${major}.${minor}.${new_patch}-SNAPSHOT" + # Update the property in pom.xml + sed -i "s|.*|${new_version}|" pom.xml + echo "Updated version to $new_version" + # Commit the version change + git config --local user.name "github-actions" + git config --local user.email "github-actions@github.com" + git add pom.xml + git commit -m "Increment version to ${new_version}" + git push origin HEAD:$GITHUB_REF_NAME + else + echo "Current version already contains -SNAPSHOT, no update needed." + fi + # Export the updated or original version + updated_version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "UPDATED_VERSION=$updated_version" >> $GITHUB_ENV + shell: bash + + - name: Print Updated Version + run: | + echo "Updated version: ${{ env.UPDATED_VERSION }}" + shell: bash + + - name: Deploy snapshot + if: ${{ endsWith(env.UPDATED_VERSION, '-SNAPSHOT') }} + run: | + mvn -B -ntp -fae -Dmaven.install.skip=true -Dmaven.test.skip=true -DdeployAtEnd=true deploy + env: + CAP_DEPLOYMENT_USER: ${{ secrets.CAP_DEPLOYMENT_USER }} + CAP_DEPLOYMENT_PASS: ${{ secrets.CAP_DEPLOYMENT_PASS }} + shell: bash +###### diff --git a/.github/workflows/pull-request-build.yml b/.github/workflows/pull-request-build.yml new file mode 100644 index 00000000..b9b6fa8b --- /dev/null +++ b/.github/workflows/pull-request-build.yml @@ -0,0 +1,29 @@ +name: Pull Request Builder + +env: + MAVEN_VERSION: '3.6.3' + +on: + pull_request: + branches: [ "develop" ] + + workflow_dispatch: + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + java-version: [ 17 ] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build + uses: ./.github/actions/build + with: + java-version: ${{ matrix.java-version }} + maven-version: ${{ env.MAVEN_VERSION }} diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml new file mode 100644 index 00000000..af0caf3a --- /dev/null +++ b/.github/workflows/sonarqube.yml @@ -0,0 +1,85 @@ +name: SonarQube Analysis + +on: + push: + branches: + - develop + pull_request: + branches: + - develop + types: [opened, synchronize, reopened] + workflow_dispatch: + +permissions: + pull-requests: read # Allows SonarQube to decorate PRs with analysis results + +jobs: + sonar-scan: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Ensure shallow clones are disabled for better analysis relevancy + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + + - name: Install dependencies + run: | + mvn clean install -P unit-tests -DskipIntegrationTests + + - name: Install SonarQube Scanner + run: | + if [ ! -L /usr/local/bin/sonar-scanner ]; then + curl -sSLo sonar-scanner-cli.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-5.0.1.3006-linux.zip + unzip sonar-scanner-cli.zip + sudo mv sonar-scanner-5.0.1.3006-linux /opt/sonar-scanner + sudo ln -s /opt/sonar-scanner/bin/sonar-scanner /usr/local/bin/sonar-scanner + fi + + - name: Run SonarQube analysis + run: | + if [ "${{ github.event_name }}" == "pull_request" ]; then + sonar-scanner \ + -Dsonar.projectKey=cap-java-sdm \ + -Dsonar.sources=sdm/src/main/java \ + -Dsonar.java.binaries=sdm/target/classes \ + -Dsonar.java.libraries=sdm/target/sdm.jar \ + -Dsonar.junit.reportPaths=sdm/target/surefire-reports \ + -Dsonar.coverage.jacoco.xmlReportPaths=sdm/target/site/jacoco/jacoco.xml \ + -Dsonar.inclusions=**/*.java \ + -Dsonar.exclusions=**/target/**,**/node_modules/**,sdm/src/main/test/**,cap-notebook/*.capnb,sdm/src/main/java/com/sap/cds/sdm/model/**,sdm/src/main/java/com/sap/cds/sdm/caching/CacheKey.java,sdm/src/main/java/com/sap/cds/sdm/caching/RepoKey.java,sdm/src/main/java/com/sap/cds/sdm/caching/TokenCacheKey.java \ + -Dsonar.java.file.suffixes=.java \ + -Dsonar.host.url=${{ secrets.SONAR_HOST_URL }} \ + -Dsonar.login=${{ secrets.SONAR_TOKEN }} \ + -Dsonar.pullrequest.key=${{ github.event.pull_request.number }} \ + -Dsonar.pullrequest.branch=${{ github.head_ref }} \ + -Dsonar.pullrequest.base=${{ github.base_ref }} + else + sonar-scanner \ + -Dsonar.projectKey=cap-java-sdm \ + -Dsonar.sources=sdm/src/main/java \ + -Dsonar.java.binaries=sdm/target/classes \ + -Dsonar.java.libraries=sdm/target/sdm.jar \ + -Dsonar.junit.reportPaths=sdm/target/surefire-reports \ + -Dsonar.coverage.jacoco.xmlReportPaths=sdm/target/site/jacoco/jacoco.xml \ + -Dsonar.inclusions=**/*.java \ + -Dsonar.exclusions=**/target/**,**/node_modules/**,sdm/src/main/test/**,cap-notebook/*.capnb,sdm/src/main/java/com/sap/cds/sdm/model/**,sdm/src/main/java/com/sap/cds/sdm/caching/CacheKey.java,sdm/src/main/java/com/sap/cds/sdm/caching/RepoKey.java,sdm/src/main/java/com/sap/cds/sdm/caching/TokenCacheKey.java \ + -Dsonar.java.file.suffixes=.java \ + -Dsonar.host.url=${{ secrets.SONAR_HOST_URL }} \ + -Dsonar.login=${{ secrets.SONAR_TOKEN }} + fi + + - name: Quality Gate Check + id: sonarqube-quality-gate + uses: sonarsource/sonarqube-quality-gate-action@master + with: + sonar_host_url: ${{ secrets.SONAR_HOST_URL }} + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/unit.tests.yml b/.github/workflows/unit.tests.yml new file mode 100644 index 00000000..e84d2404 --- /dev/null +++ b/.github/workflows/unit.tests.yml @@ -0,0 +1,45 @@ +name: UnitTestsWithCodeCoverage + +on: + push: + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] + types: [opened, synchronize, reopened, auto_merge_enabled] + +permissions: + pull-requests: read + +jobs: + unitTests: + runs-on: cap-java + strategy: + matrix: + java-version: [17] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK ${{ matrix.java-version }} + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: ${{ matrix.java-version }} + + - name: Cache Maven dependencies + uses: actions/cache@v3 + with: + path: ~/.m2 + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Install dependencies and run tests with coverage + run: | + mvn clean install -P unit-tests -DskipIntegrationTests + + - name: Upload code coverage report + uses: actions/upload-artifact@v4 + with: + name: code-coverage-report + path: target/site/jacoco/jacoco.xml diff --git a/.gitignore b/.gitignore index 524f0963..17879920 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +sdm/target/ +.flattened-pom.xml +node_modules/ + # Compiled class file *.class @@ -22,3 +26,6 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* + +### Mac System File ### +*.DS_Store \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..8a4eb1cd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,37 @@ +# Change Log + +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). +The format is based on [Keep a Changelog](http://keepachangelog.com/). + +## Version 1.0.2 + +### Added + +- Validation of special characters in attachment names. +- Implemented API requests to SDM using Cloud SDK library. + +### Fixed + +- Check for SDM roles while renaming attachments. +- Error message when a user with no SDM roles uploads an attachment. + +## Version 1.0.1 + +### Fixed + +- This plugin can be used in a multi-tenant SaaS CAP application. + +## Version 1.0.0 + +### Added + +Initial release that provides the following features + +- Create attachment : Provides the capability to upload new attachments. +- Open attachment : Provides the capability to preview attachments. +- Delete attachment : Provides the capability to remove attachments. +- Rename attachment : Provides the capability to rename attachments. +- Virus scanning : Provides the capability to support virus scan for virus scan enabled repositories. +- Draft functionality : Provides the capability of working with draft attachments. +- Display attachments specific to repository: Lists attachments contained in the repository that is configured with the CAP application. \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..e6500efc --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness towards other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +ospo@sap.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..d8204867 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,83 @@ +# Contributing + +## Code of Conduct + +All members of the project community must abide by the [Contributor Covenant, version 2.1](CODE_OF_CONDUCT.md). +Only by respecting each other we can develop a productive, collaborative community. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting [a project maintainer](.reuse/dep5). + +## Engaging in Our Project + +Thank you for your interest in contributing to the CAP plugin for SAP Document Management Service. Your assistance is much needed and appreciated. This document outlines how you can contribute, and specifies requirements you need to meet before submitting your contributions. Here are some ways in which you can contribute: + +* Reporting a bug + +* Discussing the current state of the code + +* Submitting a fix + +* Proposing new features + +We use GitHub to manage reviews of pull requests. + +* If you are a new contributor, see: [Steps to Contribute](#steps-to-contribute) + +* Before implementing your change, create an issue that describes the problem you would like to solve or the code that should be enhanced. Please note that you are willing to work on that issue. When creating an issue, make sure to label it. You can view the available labels here: [Issue labels](https://github.com/cap-java/sdm/labels) + +* The team will review the issue and decide whether it should be implemented as a pull request. In that case, they will assign the issue to you. If the team decides against picking up the issue, the team will post a comment with an explanation. + +## Issues and Planning + +* We use GitHub issues to track bugs and enhancement requests. + +* Please provide as much context as possible when you open an issue. The information you provide must be comprehensive enough to reproduce that issue for the assignee. + +## Steps to Contribute + +Should you wish to work on an issue, please claim it first by commenting on the GitHub issue that you want to work on. This is to prevent duplicated efforts from other contributors on the same issue. + +If you have questions about one of the issues, please comment on them, and one of the maintainers will clarify. + +Components that cannot be changed: + +* Licensing: Any changes to software licenses won't be accepted. +* Repository Structure: The project structure should remain unchanged unless changes are approved by the maintainers. + +Components that can be changed: + +* Documentation: If you see any gaps in the documentation, feel free to fill it. This includes walkthroughs, diagrams, typographical errors, and more. +* Bug Fixing: If you found any bugs, you can fix it and submit a pull request. +* Features: You can suggest and work on new features or enhancements to existing features. +* Performance: If you can optimise any part of the existing implementation, your contribution is greatly welcomed. + +To contribute, you can follow these steps: + +1. Fork the repo and clone it to your local system. +2. Add the original repository as a remote (use the alias "upstream"). +3. If you created your fork a while ago be sure to pull upstream changes into your local repository. +4. Create a new branch to work on from develop. +5. If you've added code that should be tested, add unit tests(/test/java/unit). Add comments to explain your changes. Make sure to check the code coverage for tests (>90%). The information on how to run unit tests will be updated shortly. +6. Once you have made your changes, push your branch commits to the forked repository. +7. From the original repository, click the "New pull request" button. +8. Select your fork and the branch you worked on. +9. The title of your PR should describe your changes. In the description, mention what you changed, why you changed it, and the issue related (with hashtag #issueNumber). Add labels to your PR such as bug, enhancement, documentation, consulting etc. +10. After raising a PR, all tests - including linting, unit tests, and integration tests - will be executed. The PR will not be merged unless all these tests pass successfully. + +Some points to keep in mind while contributing: + +1. Follow the code style of the project, including indentation. +2. Ensure proper testing of the changes. +3. Make sure the code lints. + +Once you submit a PR, the maintainers will review your submission. They might ask for changes or reject if your contribution doesn't match the project guidelines. If everything is fine, they will merge your PR. + +## Contributing Code or Documentation + +You are welcome to contribute code in order to fix a bug or to implement a new feature that is logged as an issue. + +The following rule governs code contributions: + +* Contributions must be licensed under the [Apache 2.0 License](./LICENSE) +* Due to legal reasons, contributors will be asked to accept a Developer Certificate of Origin (DCO) when they create the first pull request to this project. This happens in an automated fashion during the submission process. SAP uses [the standard DCO text of the Linux Foundation](https://developercertificate.org/). + + diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 00000000..137069b8 --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index 424ae9d5..38dc9447 100644 --- a/README.md +++ b/README.md @@ -1 +1,309 @@ -# sdm \ No newline at end of file +[![REUSE status](https://api.reuse.software/badge/github.com/cap-java/sdm)](https://api.reuse.software/info/github.com/cap-java/sdm) + +# CAP plugin for SAP Document Management Service +The `com.sap.cds:sdm` dependency is a [CAP Java plugin](https://cap.cloud.sap/docs/java/building-plugins) that provides an easy CAP-level integration with [SAP Document Management Service](https://discovery-center.cloud.sap/serviceCatalog/document-management-service-integration-option). This package supports handling of attachments(documents) by using an aspect Attachments in SAP Document Management Service. +This plugin can be consumed by the CAP application deployed on BTP to store their documents in the form of attachments in Document Management Repository. + +## Key features + +- Create attachment : Provides the capability to upload new attachments. +- Read attachment : Provides the capability to preview attachments. +- Delete attachment : Provides the capability to remove attachments. +- Rename attachment : Provides the capability to rename attachments. +- Virus scanning : Provides the capability to support virus scan for virus scan enabled repositories. +- Draft functionality : Provides the capability of working with draft attachments. +- Display attachments specific to repository: Lists attachments contained in the repository that is configured with the CAP application. + +## Table of Contents + +- [Pre-Requisites](#pre-requisites) +- [Setup](#setup) +- [Deploying and testing the application](#deploying-and-testing-the-application) +- [Use com.sap.cds:sdm dependency](#use-comsapcdssdm-dependency) +- [Known Restrictions](#known-restrictions) +- [Support, Feedback, Contributing](#support-feedback-contributing) +- [Code of Conduct](#code-of-conduct) +- [Licensing](#licensing) + +## Pre-Requisites +* Java 17 or higher +* [MTAR builder](https://www.npmjs.com/package/mbt) (`npm install -g mbt`) +* [Cloud Foundry CLI](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html), Install cf-cli and run command `cf install-plugin multiapps` +* UI5 version 1.131.0 or higher + +> **cds-services** +> +> 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. +> - 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 +> +> A reference to adding this can be found [here](https://github.com/cap-java/sdm/blob/691c329f4c3c17ae390cfcb2db1ef02650585aee/cap-notebook/demoapp/pom.xml#L20) + +## Setup + +In this guide, we use the Bookshop sample app in the [deploy branch](https://github.com/cap-java/sdm/tree/deploy) of this repository, to integrate SDM CAP plugin. Follow the steps in this section for a quick way to deploy and test the plugin without needing to create your own custom CAP application. + +### Using the released version +If you want to use the version of SDM CAP plugin released on the central maven repository follow the below steps: + +1. Remove the sdm and sdm-root folders from your local .m2 repository. This ensures that the CAP application uses the plugin version from the central Maven repository, as the local .m2 repository is prioritized during the build process. + +2. Clone the sdm repository: + +```sh + git clone https://github.com/cap-java/sdm +``` + +3. Checkout to the branch **deploy**: + +```sh + git checkout deploy +``` + +4. Navigate to the demoapp folder: + +```sh + 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**. + +6. Build the application: + +```sh + mbt build +``` +Now the application will pick the released version of the plugin from the central maven repository as the dependency is added in the [pom.xml](https://github.com/cap-java/sdm/blob/4180e501ecd792770174aa4972b06aff54ac139d/cap-notebook/demoapp/srv/pom.xml#L18) + +7. Log in to Cloud Foundry space: + +```sh + cf login -a -o -s +``` +8. Deploy the application: + +```sh + cf deploy mta_archives/*.mtar +``` + +### Using the development version +To use a development version of the SDM CAP plugin, follow these steps. This is useful if you want to test changes made in a separate branch of this github repository or use a version not yet released on the central Maven repository. + +1. Clone the sdm repository: + +```sh + git clone https://github.com/cap-java/sdm +``` +2. Install the plugin in the root folder after switiching to the branch you want to use: + +```sh + mvn clean install +``` +The plugin is now added to your local .m2 repository, giving it priority over the version available in the central Maven repository during the application build. + +3. Checkout to the branch **deploy**: + +```sh + git checkout deploy +``` + +4. Navigate to the demoapp folder: + +```sh + 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**. + +6. Build the application: + +```sh + mbt build +``` +7. Log in to Cloud Foundry space: + +```sh + cf login -a -o -s +``` +8. Deploy the application: + +```sh + cf deploy mta_archives/*.mtar +``` + +## Use com.sap.cds:sdm dependency +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 + sdm + {version} + + ``` + + To be able to also use the cds models defined in this plugin the `cds-maven-plugin` needs to be used with the + `resolve` goal to make the cds models available in the project: + + ```xml + + com.sap.cds + cds-maven-plugin + ${cds.services.version} + + + cds.resolve + + resolve + + + + + ``` + + If the cds models needs to be used in the `db` folder the `cds-maven-plugin` needs to be included also in the + `db` folder of the project. + This means the `db` folder needs to have a `pom.xml` with the `cds-maven-plugin` included and the `cds-maven-plugin` + needs to be run. + + If the `cds-maven-plugin` is used correctly and executed the following lines should be visible in the build log: + + ````log + [INFO] --- cds:3.4.1:resolve (cds.resolve) @ your-project --- + [INFO] CdsResolveMojo: Extracting models from com.sap.cds:sdm:jar::compile () + [INFO] CdsResolveMojo: Extracting models from com.sap.cds:cds-feature-attachments:jar:1.0.5:compile () + ```` + + 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 + using {my.bookshop.Books } from '../db/books'; + using {sap.attachments.Attachments} from`com.sap.cds/sdm`; + + extend entity Books with { + attachments : Composition of many Attachments; + } + ``` + +3. Create a SAP Document Management Integration Option [Service instance and key](https://help.sap.com/docs/document-management-service/sap-document-management-service/creating-service-instance-and-service-key). Bind your CAP application to this SDM instance. Add the details of this instance to the resources section in the `mta.yaml` of your CAP application. Refer the following example from a sample Bookshop app. + + ```yaml + modules: + - name: bookshop-srv + type: java + path: srv + requires: + - name: sdm-di-instance + + resources: + - name: sdm-di-instance + type: org.cloudfoundry.managed-service + parameters: + service: sdm + 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. + + ```yaml + modules: + - name: bookshop-srv + type: java + path: srv + properties: + REPOSITORY_ID: + requires: + - name: sdm-di-instance + ``` + +5. To allow the application to upload large files, add the connection and request timeouts in mta.yaml under properties of srv module. Refer the following example from a sample Bookshop app. + + ```yaml + modules: + - name: bookshop-srv + type: java + path: srv + properties: + REPOSITORY_ID: + INCOMING_CONNECTION_TIMEOUT: 900000 + INCOMING_REQUEST_TIMEOUT: 900000 + timeout: 900000 + ``` + +6. Add the following facet in _fiori-service.cds_ in the _app_ folder. Refer the following [example](https://github.com/cap-java/sdm/blob/16c1b17d521a141ef1b1adfbed1e06c5bf7a980f/cap-notebook/demoapp/app/admin-books/fiori-service.cds#L24) from a sample Bookshop app. + + ```cds + { + $Type : 'UI.ReferenceFacet', + ID : 'AttachmentsFacet', + Label : '{i18n>attachments}', + Target: 'attachments/@UI.LineItem' + } + ``` + +## Deploying and testing the application + +1. Log in to Cloud Foundry space: + + ```sh + cf login -a -o -s + ``` + +2. Build the project by running following command from root folder of your CAP application + ```sh + mbt build + ``` + Above step will generate .mtar file inside mta_archives folder. + +3. Deploy the application + ```sh + cf deploy mta_archives/*.mtar + ``` + +4. Go to your BTP subaccount and launch your application. + +5. The `Attachments` type has generated an out-of-the-box Attachments table (see highlighted box) at the bottom of the Object page: + + Attachments Table + +6. **Upload a file** by going into Edit mode by using the **Upload** button on the Attachments table. The file is then stored in SAP Document Management Integration Option. We demonstrate this by uploading a TXT file: + + Upload an attachment + +7. **Open a file** by clicking on the attachment. We demonstrate this by opening the previously uploaded TXT file: + + 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: + + Delete an attachment + +9. **Delete a file** by going into Edit mode and selecting the file(s) and by using the **Delete** button on the Attachments table. Then click the **Save** button to have that file deleted from the resource (SAP Document Management Integration Option). We demonstrate this by deleting the previously uploaded TXT file: + + Delete an attachment + +## Known Restrictions + +- Repository : This plugin does not support the use of versioned repositories. +- File size : Attachments are limited to a maximum size of 700 MB. If the repository is [onboarded](https://help.sap.com/docs/document-management-service/sap-document-management-service/internal-repository?version=Cloud&locale=en-US) with virus scan enabled for all files, attachments are limited to a maximum size of 400 MB. + +## Support, Feedback, Contributing + +This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/cap-java/sdm/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md). + +## Code of Conduct + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](CODE_OF_CONDUCT.md) at all times. + +## 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). + diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 00000000..8235b1c4 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,11 @@ +version = 1 +SPDX-PackageName = "sdm" +SPDX-PackageSupplier = "The CAP team " +SPDX-PackageDownloadLocation = "https://github.com/cap-java/sdm" +SPDX-PackageComment = "The code in this project may include calls to APIs (\"API Calls\") of\n SAP or third-party products or services developed outside of this project\n (\"External Products\").\n \"APIs\" means application programming interfaces, as well as their respective\n specifications and implementing code that allows software to communicate with\n other software.\n API Calls to External Products are not licensed under the open source license\n that governs this project. The use of such API Calls and related External\n Products are subject to applicable additional agreements with the relevant\n provider of the External Products. In no event shall the open source license\n that governs this project grant any rights in or to any External Products,or\n alter, expand or supersede any terms of the applicable additional agreements.\n If you have a valid license agreement with SAP for the use of a particular SAP\n External Product, then you may make use of any API Calls included in this\n project's code for that SAP External Product, subject to the terms of such\n license agreement. If you do not have a valid license agreement for the use of\n a particular SAP External Product, then you may only make use of any API Calls\n in this project for that SAP External Product for your internal, non-productive\n and non-commercial test and evaluation of such API Calls. Nothing herein grants\n you any rights to use or access any SAP External Product, or provide any third\n parties the right to use of access any SAP External Product, through API Calls." + +[[annotations]] +path = "**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2024 SAP SE or an SAP affiliate company and sdm contributors" +SPDX-License-Identifier = "Apache-2.0" diff --git a/cap-notebook/attachments-demo-app.capnb b/cap-notebook/attachments-demo-app.capnb new file mode 100644 index 00000000..13a6377c --- /dev/null +++ b/cap-notebook/attachments-demo-app.capnb @@ -0,0 +1,244 @@ +[ + { + "kind": 1, + "language": "markdown", + "value": "# CDS SDM CAP Notebook\n\nThis CAP notebook creates a CAP Java demoapp with sample data and enhances the app with the CAP feature for attachments.\nAll needed enhancements are done. \nFor more information check the project [README](../README.md). ", + "outputs": [] + }, + { + "kind": 1, + "language": "markdown", + "value": "## Add the App with Sample Data\n`cds init` is used to create a basic CAP Java app with sample data.", + "outputs": [] + }, + { + "kind": 2, + "language": "shell", + "value": "cds init demoapp --add java,sample\n", + "outputs": [ + { + "mime": "text/plain", + "value": "Creating new CAP project" + } + ] + }, + { + "kind": 2, + "language": "shell", + "value": "cd demoapp", + "outputs": [ + { + "mime": "text/plain", + "value": "\n" + } + ] + }, + { + "kind": 1, + "language": "markdown", + "value": "## Add Enhancements for the Datamodel\nThe `books` entity will be enhanced with the `attachments` composition.\n\nTo be able to use the `sdm` datamodel a `pom.xml` needs to be added with the maven dependency for the feature.\nThe version for the dependency is taken from the file `version.txt`. \nThis file will be updated if a new version is created in the repository.\n\nOnce the `pom.xml` is available and the version is set a `mvn clean verify` is executed.\nWith the the `resolve` goal of the `cds-maven-plugin` is executed which copies the `cds`-files from the feature in the `target` folder of the `db` module.\n\nOnce available in the `target` folder it will be found and can be used in the data models.", + "outputs": [] + }, + { + "kind": 2, + "language": "shell", + "value": "%%writefile \"db/attachment-extension.cds\"\nusing {sap.capire.bookshop.Books} from './schema';\nusing {sap.attachments.Attachments} from`com.sap.cds/sdm`;\n\nextend entity Books with {\n attachments : Composition of many Attachments;\n}\n\nentity Statuses @cds.autoexpose @readonly {\n key code : StatusCode;\n text : localized String(255);\n}\n\nextend Attachments with {\n statusText : Association to Statuses on statusText.code = $self.status;\n}\n\nannotate Books.attachments with {\n status @(\n Common.Text: {\n $value: ![statusText.text],\n ![@UI.TextArrangement]: #TextOnly\n },\n ValueList: {entity:'Statuses'},\n sap.value.list: 'fixed-values'\n );\n}\n", + "outputs": [ + { + "mime": "text/html", + "value": "Wrote cell content to file demoapp/db/attachment-extension.cds.\n" + } + ] + }, + { + "kind": 2, + "language": "shell", + "value": "", + "outputs": [ + { + "mime": "text/html", + "value": "Wrote cell content to file demoapp/db/data/Statuses.csv.\n" + } + ] + }, + { + "kind": 2, + "language": "shell", + "value": "", + "outputs": [ + { + "mime": "text/html", + "value": "Wrote cell content to file demoapp/db/data/Statuses_texts.csv.\n" + } + ] + }, + { + "kind": 2, + "language": "shell", + "value": "%%writefile \"db/pom.xml\"\n\n\n\t4.0.0\n\t\n\t\tdemoapp-parent\n\t\tcustomer\n\t\t${revision}\n\t\n\n\tdb\n\n \n \n \n com.sap.cds\n sdm\n 1.0.0\n \n \n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\tcom.sap.cds\n\t\t\t\tcds-maven-plugin\n\t\t\t\t${cds.services.version}\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tcds.clean\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tclean\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tcds.resolve\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tresolve\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t\n\n", + "outputs": [ + { + "mime": "text/html", + "value": "Wrote cell content to file demoapp/db/pom.xml.\n" + } + ] + }, + { + "kind": 2, + "language": "shell", + "value": "cd db", + "outputs": [ + { + "mime": "text/plain", + "value": "\n" + } + ] + }, + { + "kind": 2, + "language": "java", + "value": "Path versionPath = Paths.get(\"../../version.txt\");\nString version;\nif (Files.exists(versionPath)){\n version = Files.readString(versionPath);\n System.out.println(\"Using version from 'version.txt': \" + version);\n}else{\n version = \"1.0.2\";\n System.out.println(\"Using hard coded version: \" + version);\n}\nPath pomPath = Paths.get(\"pom.xml\");\nStream lines = Files.lines(pomPath);\nList replaced = lines.map(line -> line.replaceAll(\"attachment_version\", version)).collect(Collectors.toList());\nFiles.write(pomPath, replaced);\nlines.close();", + "outputs": [ + { + "mime": "text/plain", + "value": "Using version from 'version.txt': 1.0.2\n\n\n" + } + ] + }, + { + "kind": 2, + "language": "shell", + "value": "mvn clean compile", + "outputs": [ + { + "mime": "text/plain", + "value": "" + } + ] + }, + { + "kind": 1, + "language": "markdown", + "value": "## Service Changes\n\nThe service module `srv` of the demo project needs to be updated with the maven dependency for `sdm`.\nThis dependency has included the logic to correctly handle attachments and call the `AtacchmentService`.\n\nAlso here, the version is taken from the `version.txt` which is updated in case a new version in the repository is created.", + "outputs": [] + }, + { + "kind": 2, + "language": "shell", + "value": "cd ../srv", + "outputs": [ + { + "mime": "text/plain", + "value": "\n" + } + ] + }, + { + "kind": 1, + "language": "markdown", + "value": "add the following dependency to the `srv/pom.xml`:\n```\n\n com.sap.cds\n sdm\n 1.0.0-SNAPSHOT\n\n``` ", + "outputs": [] + }, + { + "kind": 2, + "language": "java", + "value": "\nPath versionPath = Paths.get(\"../../version.txt\");\nString version;\nif (Files.exists(versionPath)){\n version = Files.readString(versionPath);\n System.out.println(\"Using version from 'version.txt': \" + version);\n}else{\n version = \"1.0.2\";\n System.out.println(\"Using hard coded version: \" + version);\n}\n\nString filePath = \"pom.xml\";\ntry {\n String pom = Files.readString(Path.of(filePath));\n String searchString = \"\";\n Pattern pattern = Pattern.compile(searchString);\n Matcher matcher = pattern.matcher(pom);\n\n if (matcher.find()) {\n System.out.println(\"String found at position: \" + matcher.start());\n } else {\n System.out.println(\"String not found\");\n }\n\n String newDependency = \"\\n\\n \\n com.sap.cds\\n sdm\\n \" + version + \"\\n \\n\\n\";\n int insertPos = matcher.end();\n pom = pom.substring(0, insertPos) + newDependency + pom.substring(insertPos);\n\n Files.writeString(Path.of(filePath), pom);\n\n} catch (IOException e) {\n e.printStackTrace();\n}", + "outputs": [ + { + "mime": "text/plain", + "value": "Using version from 'version.txt': 1.0.2\n\nString found at position: 540\n\n" + } + ] + }, + { + "kind": 2, + "language": "shell", + "value": "cd ..", + "outputs": [ + { + "mime": "text/plain", + "value": "\n" + } + ] + }, + { + "kind": 1, + "language": "markdown", + "value": "## UI Enhancements\n", + "outputs": [] + }, + { + "kind": 1, + "language": "markdown", + "value": "### UI Facet\n\nA UI facet is added for the attachments in the `AdminService`. Because the facet is only added in this service, only this services shows the attachments on the UI.\n\nThe following facet is added:\n\n```\n{\n $Type : 'UI.ReferenceFacet',\n ID : 'AttachmentsFacet',\n Label : '{i18n>attachments}',\n Target: 'attachments/@UI.LineItem'\\n \n}\n```", + "outputs": [] + }, + { + "kind": 2, + "language": "java", + "value": "String filePath = \"app/admin-books/fiori-service.cds\";\n\ntry {\n String cds = Files.readString(Path.of(filePath));\n String searchString = \"Target:\\\\s*'@UI\\\\.FieldGroup#Details'\\\\s*},\";\n Pattern pattern = Pattern.compile(searchString);\n Matcher matcher = pattern.matcher(cds);\n\n if (matcher.find()) {\n System.out.println(\"String found at position: \" + matcher.start());\n } else {\n System.out.println(\"String not found\");\n }\n\n String newFacet = \"\\n {\\n $Type : 'UI.ReferenceFacet',\\n ID : 'AttachmentsFacet',\\n Label : '{i18n>attachments}',\\n Target: 'attachments/@UI.LineItem'\\n },\";\n int insertPos = matcher.end();\n cds = cds.substring(0, insertPos) + newFacet + cds.substring(insertPos);\n\n Files.writeString(Path.of(filePath), cds);\n\n} catch (IOException e) {\n e.printStackTrace();\n}", + "outputs": [ + { + "mime": "text/plain", + "value": "String found at position: 546\n\n" + } + ] + }, + { + "kind": 1, + "language": "markdown", + "value": "### Texts\n\nThe i18n property file is enhanced with the texts for the attachments to show correct texts on the UI.", + "outputs": [] + }, + { + "kind": 2, + "language": "shell", + "value": "cd app/_i18n", + "outputs": [ + { + "mime": "text/plain", + "value": "\n" + } + ] + }, + { + "kind": 2, + "language": "java", + "value": "String filePath = \"i18n.properties\";\n\nList properties = new ArrayList<>();\nproperties.add(\"\\n\");\nproperties.add(\"#Attachment properties\\n\");\nproperties.add(\"attachment_content = Content\\n\");\nproperties.add(\"attachment_mimeType = Mime Type\\n\");\nproperties.add(\"attachment_fileName = File Name\\n\");\nproperties.add(\"attachment_status = Status\\n\");\nproperties.add(\"attachment_note = Notes\\n\");\nproperties.add(\"attachment = Attachment\\n\");\nproperties.add(\"attachments = Attachments\");\n\nfor (String property: properties){\n try {\n Files.write(Paths.get(filePath), property.getBytes(), StandardOpenOption.APPEND);\n } catch (IOException e) {\n e.printStackTrace();\n }\n}\n", + "outputs": [ + { + "mime": "text/plain", + "value": "\n" + } + ] + }, + { + "kind": 1, + "language": "markdown", + "value": "## Build the Service\n\nRun `mvn clean compile` on the service to compile the models with all changes.", + "outputs": [] + }, + { + "kind": 2, + "language": "shell", + "value": "cd ../../srv\nmvn clean compile", + "outputs": [ + { + "mime": "text/plain", + "value": "" + } + ] + }, + { + "kind": 1, + "language": "markdown", + "value": "## Start the Service\n\n\nThe service can now be started with the following command in the `srv` module:\n\n```\nmvn cds:watch\n```\n\nAfter the service is startet the UI can be opened with:\n\n[http://localhost:8080](http://localhost:8080)\n\nNavigate to the index.html of the webapp and use user `admin` with password `admin`. \n\nUsing the tile `Manage Books` the attachments can be used in the detail area of the books.\n\nUsing the tile `Browse Books` no attachments are shown.", + "outputs": [] + }, + { + "kind": 1, + "language": "markdown", + "value": "", + "outputs": [] + } +] \ No newline at end of file diff --git a/deploy-oss/pom.xml b/deploy-oss/pom.xml new file mode 100644 index 00000000..3439abe1 --- /dev/null +++ b/deploy-oss/pom.xml @@ -0,0 +1,96 @@ + + 4.0.0 + com.sap.cds + sdm-root + ${revision} + pom + + Deploy to OSS + This artifact can be used to deploy all required artifacts of sdm to OSS Nexus + + + true + true + + + + + + maven-install-plugin + 3.1.3 + + + install-sdm + install + + install-file + + + ${project.groupId} + sdm + jar + ../sdm/target/sdm.jar + ../sdm/target/sdm-sources.jar + ../sdm/target/sdm-javadoc.jar + ../sdm/.flattened-pom.xml + ${revision} + + + + install-sdm-root + install + + install-file + + + ${project.groupId} + sdm-root + pom + ../.flattened-pom.xml + ../.flattened-pom.xml + ${revision} + + + + + + maven-gpg-plugin + 3.2.6 + + + deploy-sdm + deploy + + sign-and-deploy-file + + + ${project.groupId} + sdm + jar + ../sdm/target/sdm.jar + ../sdm/target/sdm-sources.jar + ../sdm/target/sdm-javadoc.jar + ../sdm/.flattened-pom.xml + ${revision} + + + + deploy-sdm-root + deploy + + sign-and-deploy-file + + + ${project.groupId} + sdm-root + pom + ../.flattened-pom.xml + ../.flattened-pom.xml + ${revision} + + + + + + + diff --git a/ensure-license.sh b/ensure-license.sh new file mode 100644 index 00000000..1c115495 --- /dev/null +++ b/ensure-license.sh @@ -0,0 +1,43 @@ +# Requires bash in version > 4.0.0 +shopt -s globstar + +year=$(date +"%Y") +license_indicator="SAP SE or an SAP affiliate company. All rights reserved." +license1="**************************************************************************" +license2=" * (C) 2019-$year $license_indicator *" +license3=" **************************************************************************" +java_files=$(printf %s\\n */src/main/java/**/*.java) + +write_license () { + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "1i\\ +/$license1\\ +$license2\\ +$license3/\\ +" $file + else + sed -i "1i/$license1\n$license2\n$license3/" $1 + fi +} + +update_license () { + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "1s/.*/\/$license1/" $1 + sed -i '' "2s/.*/$license2/" $1 + sed -i '' "3s/.*/$license3\//" $1 + else + sed -i "1s/.*/\/$license1/" $1 + sed -i "2s/.*/$license2/" $1 + sed -i "3s/.*/$license3\//" $1 + fi +} + +while read -r file; do + if ! grep -q -F "$license_indicator" "$file"; then + echo "Writing license to '$file'" + write_license $file + else + echo "Updating license in '$file'" + update_license $file + fi +done <<< "$java_files" diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..484b9dad --- /dev/null +++ b/pom.xml @@ -0,0 +1,247 @@ + + 4.0.0 + + + + The Apache Software License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + SAP SE + https://www.sap.com + + + + + SAP SE + https://www.sap.com + + + + + 1.0.2 + 17 + ${java.version} + ${java.version} + UTF-8 + + sdm/generated/ + 3.5.0 + + src/gen + + 3.2.5 + 5.15.0 + + + com.sap.cds + sdm-root + ${revision} + pom + + CDS Feature for SAP Document Management Service - Root + This artifact is a is cds-plugin that provides an easy CAP-level integration with SAP Document Management Service. This package supports handling of attachments(documents) by using an aspect Attachments in SAP Document Management Service. + https://cap.cloud.sap/docs/plugins/#attachments + + + sdm + + + + + + com.sap.cds + cds-services-bom + ${cds.services.version} + pom + import + + + + org.mockito + mockito-bom + 5.12.0 + pom + import + + + + com.sap.cds + sdm + ${revision} + + + com.sap.cloud.environment.servicebinding + java-bom + 0.10.5 + pom + import + + + com.sap.cloud.sdk + sdk-bom + ${sdk-bom-version} + pom + import + + + + + + + + com.sap.cds + cds-services-api + + + + org.junit.jupiter + junit-jupiter-api + 5.11.3 + test + + + org.assertj + assertj-core + 3.25.3 + test + + + + org.mockito + mockito-core + 5.12.0 + test + + + org.apache.httpcomponents + httpmime + 4.5.14 + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + repoid + + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.43.0 + + + + src/main/java/**/*.java + src/test/java/**/*.java + + + + + + + + + + check + + compile + + + + + + + org.codehaus.mojo + flatten-maven-plugin + 1.6.0 + + true + resolveCiFriendliesOnly + + + + flatten + process-resources + + flatten + + + + flatten.clean + clean + + clean + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.4.1 + + + no-duplicate-declared-dependencies + + enforce + + + + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.2.0 + + + + analyze + + + true + + + + + + + + + + artifactory + Artifactory_DMZ-snapshots + https://common.repositories.cloud.sap/artifactory/cap-sdm-java + + + artifactory + Artifactory_DMZ + https://common.repositories.cloud.sap/artifactory/cap-sdm-java + + + + + https://github.com/cap-java/sdm + scm:git:git@github.com:cap-java/sdm.git + scm:git:git@github.com:cap-java/sdm.git + + + diff --git a/resources/attachments.png b/resources/attachments.png new file mode 100644 index 00000000..49fb144a Binary files /dev/null and b/resources/attachments.png differ diff --git a/resources/create.gif b/resources/create.gif new file mode 100644 index 00000000..25ecaae8 Binary files /dev/null and b/resources/create.gif differ diff --git a/resources/delete.gif b/resources/delete.gif new file mode 100644 index 00000000..df31f80f Binary files /dev/null and b/resources/delete.gif differ diff --git a/resources/read.gif b/resources/read.gif new file mode 100644 index 00000000..3461ac0d Binary files /dev/null and b/resources/read.gif differ diff --git a/resources/rename.gif b/resources/rename.gif new file mode 100644 index 00000000..a2f256b2 Binary files /dev/null and b/resources/rename.gif differ diff --git a/sdm/pom.xml b/sdm/pom.xml new file mode 100644 index 00000000..a70adc71 --- /dev/null +++ b/sdm/pom.xml @@ -0,0 +1,476 @@ + + 4.0.0 + + + com.sap.cds + sdm-root + ${revision} + + + + + The Apache Software License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + SAP SE + https://www.sap.com + + + sdm + jar + + CDS Feature for SAP Document Management Service + https://cap.cloud.sap/docs/plugins/#attachments + + + sdm + 7.6.0 + com.sap.cds.sdm.generated + src/test/gen + 17 + 17 + 1.0.5 + 1.18.36 + 0.8.7 + 1.17.2 + 3.10.8 + + + + + unit-tests + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + **/unit/**/*.java + + + + + + + + integration-tests + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.3.1 + + + integration-test + + integration-test + verify + + + + **/integration/**/*.java + + + + + + + + + + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + com.sap.cloud.security.xsuaa + token-client + 3.5.3 + + + com.sap.cloud.environment.servicebinding.api + java-access-api + 0.10.5 + + + com.sap.cloud.environment.servicebinding.api + java-core-api + 0.10.5 + + + org.apache.httpcomponents.core5 + httpcore5 + 5.3.1 + + + com.fasterxml.jackson.core + jackson-databind + 2.18.0 + + + org.json + json + 20240303 + + + org.apache.httpcomponents.client5 + httpclient5 + 5.4.1 + + + org.ehcache + ehcache + ${ehcache-version} + + + org.glassfish.jaxb + jaxb-runtime + + + + + commons-io + commons-io + 2.17.0 + + + com.fasterxml.jackson.core + jackson-annotations + 2.18.0 + + + com.google.code.gson + gson + 2.11.0 + + + org.slf4j + slf4j-api + 2.0.13 + + + + + com.fasterxml.jackson.core + jackson-core + 2.17.2 + + + com.sap.cds + cds-services-impl + test + + + junit + junit + 4.13.2 + test + + + com.squareup.okhttp3 + okhttp + 5.0.0-alpha.14 + + + com.squareup.okhttp3 + mockwebserver + 5.0.0-alpha.14 + test + + + com.sap.cds + cds-feature-attachments + ${attachments_version} + + + jakarta.jms + jakarta.jms-api + + + + + commons-codec + commons-codec + ${commons-codec.version} + + + org.mockito + mockito-junit-jupiter + 5.12.0 + test + + + com.sap.cds + cds-starter-cloudfoundry + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.security + spring-security-core + + + org.springframework.security + spring-security-web + + + org.springframework + spring-web + + + org.springframework + spring-core + + + + + + + ${project.artifactId} + + + maven-javadoc-plugin + + ${skipDuringDeploy} + true + all,-missing + + + + + jar + + + com.sap.sdm.generated.* + + + + + + org.apache.maven.plugins + maven-clean-plugin + 3.3.2 + + + + auto-clean + clean + + clean + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + ${skipUnitTests} + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.3.1 + + ${skipIntegrationTests} + + **/integration/**/*.java + + + + + + com.sap.cds + cds-maven-plugin + ${cds.services.version} + + ${skipDuringDeploy} + + + + cds.clean + + clean + + + + + cds.install-node + + install-node + + + ${skipDuringDeploy} + + + + + cds.install-cdsdk + + install-cdsdk + + + ${skipDuringDeploy} + + + + + cds.build + + cds + + + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + + ${excluded.generation.package}**/* + + + com/sap/cds/sdm/constants/** + + + com/sap/cds/sdm/model/** + + + com/sap/cds/sdm/persistence/** + + + com/sap/cds/sdm/service/SDMAttachmentsService.class + + + com/sap/cds/sdm/caching/** + + + + + + jacoco-initialize + + prepare-agent + + + + jacoco-site-report-all-tests + verify + + report + + + + jacoco-site-report-only-unit-tests + test + + report + + + + jacoco-check-unit-tests-only + test + + check + + + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 0.90 + + + BRANCH + COVEREDRATIO + 0.90 + + + CLASS + MISSEDCOUNT + 0 + + + + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.5.0 + + + generate-sources + + add-source + + + + ${project.basedir}/${generation-folder}/java + + + + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + + jar + + + + + + + + + + + + artifactory + Artifactory_DMZ-snapshots + https://common.repositories.cloud.sap/artifactory/cap-sdm-java + + + + diff --git a/sdm/src/main/java/com/sap/cds/sdm/caching/CacheConfig.java b/sdm/src/main/java/com/sap/cds/sdm/caching/CacheConfig.java new file mode 100644 index 00000000..8e899910 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/caching/CacheConfig.java @@ -0,0 +1,83 @@ +package com.sap.cds.sdm.caching; + +import java.util.concurrent.TimeUnit; +import org.ehcache.Cache; +import org.ehcache.CacheManager; +import org.ehcache.config.builders.*; +import org.ehcache.expiry.Duration; +import org.ehcache.expiry.Expirations; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CacheConfig { + + private static CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(); + private static Cache userTokenCache; + private static Cache clientCredentialsTokenCache; + private static Cache userAuthoritiesTokenCache; + private static Cache versionedRepoCache; + private static final int HEAP_SIZE = 1000; + private static final int USER_TOKEN_EXPIRY = 660; + private static final int ACCESS_TOKEN_EXPIRY = 660; + private static final Logger logger = LoggerFactory.getLogger(CacheConfig.class); + + private CacheConfig() { + throw new IllegalStateException("CacheConfig class"); + } + + public static void initializeCache() { + // Expiring the cache after 11 hours + logger.info("Cache for user token and access token initialized"); + cacheManager.init(); + + userTokenCache = + cacheManager.createCache( + "userToken", + CacheConfigurationBuilder.newCacheConfigurationBuilder( + CacheKey.class, String.class, ResourcePoolsBuilder.heap(HEAP_SIZE)) + .withExpiry( + Expirations.timeToLiveExpiration( + new Duration(USER_TOKEN_EXPIRY, TimeUnit.MINUTES)))); + clientCredentialsTokenCache = + cacheManager.createCache( + "clientCredentialsToken", + CacheConfigurationBuilder.newCacheConfigurationBuilder( + CacheKey.class, String.class, ResourcePoolsBuilder.heap(HEAP_SIZE)) + .withExpiry( + Expirations.timeToLiveExpiration( + new Duration(ACCESS_TOKEN_EXPIRY, TimeUnit.MINUTES)))); + versionedRepoCache = + cacheManager.createCache( + "versionedRepo", + CacheConfigurationBuilder.newCacheConfigurationBuilder( + RepoKey.class, String.class, ResourcePoolsBuilder.heap(HEAP_SIZE)) + .withExpiry( + Expirations.timeToLiveExpiration( + new Duration(ACCESS_TOKEN_EXPIRY, TimeUnit.MINUTES)))); + + userAuthoritiesTokenCache = + cacheManager.createCache( + "userAuthoritiesToken", + CacheConfigurationBuilder.newCacheConfigurationBuilder( + TokenCacheKey.class, String.class, ResourcePoolsBuilder.heap(HEAP_SIZE)) + .withExpiry( + Expirations.timeToLiveExpiration( + new Duration(USER_TOKEN_EXPIRY, TimeUnit.MINUTES)))); + } + + public static Cache getUserTokenCache() { + return userTokenCache; + } + + public static Cache getUserAuthoritiesTokenCache() { + return userAuthoritiesTokenCache; + } + + public static Cache getClientCredentialsTokenCache() { + return clientCredentialsTokenCache; + } + + public static Cache getVersionedRepoCache() { + return versionedRepoCache; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/caching/CacheKey.java b/sdm/src/main/java/com/sap/cds/sdm/caching/CacheKey.java new file mode 100644 index 00000000..f9f612e4 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/caching/CacheKey.java @@ -0,0 +1,13 @@ +package com.sap.cds.sdm.caching; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CacheKey { + private String key; + private String expiration; +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/caching/RepoKey.java b/sdm/src/main/java/com/sap/cds/sdm/caching/RepoKey.java new file mode 100644 index 00000000..f946d8cb --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/caching/RepoKey.java @@ -0,0 +1,13 @@ +package com.sap.cds.sdm.caching; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RepoKey { + private String repoId; + private String subdomain; +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/caching/TokenCacheKey.java b/sdm/src/main/java/com/sap/cds/sdm/caching/TokenCacheKey.java new file mode 100644 index 00000000..7c0481cc --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/caching/TokenCacheKey.java @@ -0,0 +1,12 @@ +package com.sap.cds.sdm.caching; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TokenCacheKey { + private String key; +} 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 new file mode 100644 index 00000000..f1e0acaf --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/configuration/Registration.java @@ -0,0 +1,87 @@ +package com.sap.cds.sdm.configuration; + +import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.sdm.caching.CacheConfig; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.applicationservice.SDMCreateAttachmentsHandler; +import com.sap.cds.sdm.handler.applicationservice.SDMReadAttachmentsHandler; +import com.sap.cds.sdm.handler.applicationservice.SDMUpdateAttachmentsHandler; +import com.sap.cds.sdm.service.SDMAttachmentsService; +import com.sap.cds.sdm.service.SDMService; +import com.sap.cds.sdm.service.SDMServiceImpl; +import com.sap.cds.sdm.service.handler.SDMAttachmentsServiceHandler; +import com.sap.cds.services.environment.CdsEnvironment; +import com.sap.cds.services.environment.CdsProperties; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.persistence.PersistenceService; +import com.sap.cds.services.runtime.CdsRuntime; +import com.sap.cds.services.runtime.CdsRuntimeConfiguration; +import com.sap.cds.services.runtime.CdsRuntimeConfigurer; +import com.sap.cds.services.utils.environment.ServiceBindingUtils; +import com.sap.cloud.environment.servicebinding.api.ServiceBinding; +import java.time.Duration; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The class {@link Registration} is a configuration class that registers the services and event + * handlers for the attachments feature. + */ +public class Registration implements CdsRuntimeConfiguration { + + private static final Logger logger = LoggerFactory.getLogger(Registration.class); + + @Override + public void services(CdsRuntimeConfigurer configurer) { + configurer.service(buildAttachmentService()); + } + + @Override + public void eventHandlers(CdsRuntimeConfigurer configurer) { + logger.info("Registering event handler for attachment service"); + CacheConfig.initializeCache(); + CdsRuntime runtime = configurer.getCdsRuntime(); + CdsEnvironment environment = runtime.getEnvironment(); + var persistenceService = + configurer + .getCdsRuntime() + .getServiceCatalog() + .getService(PersistenceService.class, PersistenceService.DEFAULT_NAME); + List bindings = + environment + .getServiceBindings() + .filter(b -> ServiceBindingUtils.matches(b, SDMConstants.SDM_ENV_NAME)) + .toList(); + var binding = !bindings.isEmpty() ? bindings.get(0) : null; + + // get HTTP connection pool configuration + var connectionPool = getConnectionPool(environment); + + SDMService sdmService = new SDMServiceImpl(binding, connectionPool); + configurer.eventHandler(buildReadHandler()); + configurer.eventHandler(new SDMCreateAttachmentsHandler(sdmService)); + configurer.eventHandler(new SDMUpdateAttachmentsHandler(persistenceService, sdmService)); + configurer.eventHandler(new SDMAttachmentsServiceHandler(persistenceService, sdmService)); + } + + private AttachmentService buildAttachmentService() { + logger.info("Registering SDM attachment service"); + return new SDMAttachmentsService(); + } + + private static CdsProperties.ConnectionPool getConnectionPool(CdsEnvironment env) { + // the common prefix for the connection pool configuration + final String prefix = SDMConstants.SDM_CONNECTIONPOOL_PREFIX; + Duration timeout = + Duration.ofSeconds(env.getProperty(prefix.formatted("timeout"), Integer.class, 1200)); + int maxConnections = env.getProperty(prefix.formatted("maxConnections"), Integer.class, 100); + logger.debug( + "Connection pool configuration: timeout={}, maxConnections={}", timeout, maxConnections); + return new CdsProperties.ConnectionPool(timeout, maxConnections, maxConnections); + } + + protected EventHandler buildReadHandler() { + return new SDMReadAttachmentsHandler(); + } +} 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 new file mode 100644 index 00000000..bbedcaf2 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/constants/SDMConstants.java @@ -0,0 +1,70 @@ +package com.sap.cds.sdm.constants; + +import java.util.List; + +public class SDMConstants { + private SDMConstants() { + // Doesn't do anything + } + + public static final String REPOSITORY_ID = System.getenv("REPOSITORY_ID"); + 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_RENAME_THE_ATTACHMENT = "Could not rename the attachment"; + public static final String ATTACHMENT_NOT_FOUND = "Attachment not found"; + public static final String DUPLICATE_FILES_ERROR = "%s already exists."; + 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_ERROR = "%s contains potential malware and cannot be uploaded."; + public static final String REPOSITORY_ERROR = "Failed to get repository info."; + public static final String NOT_FOUND_ERROR = "Failed to read document."; + public static final String NAME_CONSTRAINT_WARNING_MESSAGE = + "Enter a valid file name for %s. The following characters are not supported: /, \\"; + public static final String SDM_MISSING_ROLES_EXCEPTION_MSG = + "You do not have the required permissions to rename 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 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_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 FILE_NOT_FOUND_ERROR = "Object not found in repository"; + + public static String nameConstraintMessage( + List fileNameWithRestrictedCharacters, String operation) { + // Create the base message + String prefixMessage = + "%s unsuccessful. The following file names contain unsupported characters (/, \\). \n\n"; + + // Create the formatted prefix message + String formattedPrefixMessage = String.format(prefixMessage, operation); + + // Initialize the StringBuilder with the formatted message prefix + StringBuilder bulletPoints = new StringBuilder(formattedPrefixMessage); + + // Append each unsupported file name to the StringBuilder + for (String file : fileNameWithRestrictedCharacters) { + bulletPoints.append(String.format("\t• %s%n", file)); + } + bulletPoints.append("\nRename the files and try again."); + return bulletPoints.toString(); + } + + public static String getDuplicateFilesError(String filename) { + return String.format(DUPLICATE_FILES_ERROR, filename); + } + + 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/handler/TokenHandler.java b/sdm/src/main/java/com/sap/cds/sdm/handler/TokenHandler.java new file mode 100644 index 00000000..c53941ec --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/TokenHandler.java @@ -0,0 +1,197 @@ +package com.sap.cds.sdm.handler; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.sap.cds.sdm.caching.CacheConfig; +import com.sap.cds.sdm.caching.TokenCacheKey; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.model.SDMCredentials; +import com.sap.cds.services.environment.CdsProperties; +import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingAccessor; +import com.sap.cloud.environment.servicebinding.api.ServiceBinding; +import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpClientFactory; +import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; +import com.sap.cloud.sdk.cloudplatform.connectivity.OAuth2DestinationBuilder; +import com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf; +import com.sap.cloud.security.config.ClientCredentials; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.commons.codec.binary.Base64; +import org.apache.http.client.HttpClient; + +public class TokenHandler { + + private static final ObjectMapper mapper = new ObjectMapper(); + + private TokenHandler() { + throw new IllegalStateException("TokenHandler class"); + } + + public static byte[] toBytes(String str) { + return requireNonNull(str).getBytes(StandardCharsets.UTF_8); + } + + public static String toString(byte[] bytes) { + return new String(requireNonNull(bytes), StandardCharsets.UTF_8); + } + + private static final String SDM_TOKEN_ENDPOINT = "url"; + private static final String SDM_URL = "uri"; + private static final String CLIENT_ID = "clientid"; + private static final String CLIENT_SECRET = "clientsecret"; + + public static SDMCredentials getSDMCredentials() { + List allServiceBindings = + DefaultServiceBindingAccessor.getInstance().getServiceBindings(); + // filter for a specific binding + ServiceBinding sdmBinding = + allServiceBindings.stream() + .filter(binding -> "sdm".equalsIgnoreCase(binding.getServiceName().orElse(null))) + .findFirst() + .get(); + SDMCredentials sdmCredentials = new SDMCredentials(); + Map uaaCredentials = sdmBinding.getCredentials(); + Map uaa = (Map) uaaCredentials.get("uaa"); + + sdmCredentials.setBaseTokenUrl(uaa.get("url").toString()); + sdmCredentials.setUrl(sdmBinding.getCredentials().get("uri").toString()); + sdmCredentials.setClientId(uaa.get("clientid").toString()); + sdmCredentials.setClientSecret(uaa.get("clientsecret").toString()); + return sdmCredentials; + } + + public static String getUserTokenFromAuthorities( + String email, String subdomain, SDMCredentials sdmCredentials) throws IOException { + // Fetch the token from Cache if present use it else generate and store + String cachedToken = null; + String userCredentials = sdmCredentials.getClientId() + ":" + sdmCredentials.getClientSecret(); + String authHeaderValue = "Basic " + Base64.encodeBase64String(toBytes(userCredentials)); + // Define the authorities (JSON) and URL encode it + String authoritiesJson = + "{\"az_attr\":{\"X-EcmUserEnc\":" + email + ",\"X-EcmAddPrincipals\":" + email + "}}"; + String encodedAuthorities = + URLEncoder.encode(authoritiesJson, StandardCharsets.UTF_8.toString()); + + // Create body parameters including the grant type and authorities + String bodyParams = "grant_type=client_credentials&authorities=" + encodedAuthorities; + byte[] postData = bodyParams.getBytes(StandardCharsets.UTF_8); + String baseTokenUrl = sdmCredentials.getBaseTokenUrl(); + if (subdomain != null && !subdomain.equals("")) { + String providersubdomain = + baseTokenUrl.substring(baseTokenUrl.indexOf("/") + 2, baseTokenUrl.indexOf(".")); + baseTokenUrl = baseTokenUrl.replace(providersubdomain, subdomain); + } + // Create the URL for the token endpoint + String authUrl = baseTokenUrl + "/oauth/token"; + URL url = new URL(authUrl); + + // Open the connection and set the properties + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("Authorization", authHeaderValue); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("charset", "utf-8"); + conn.setRequestProperty("Content-Length", String.valueOf(postData.length)); + conn.setUseCaches(false); + conn.setDoInput(true); + conn.setDoOutput(true); + + // Write the POST data to the output stream + try (DataOutputStream os = new DataOutputStream(conn.getOutputStream())) { + os.write(postData); + } + String resp; + try (DataInputStream is = new DataInputStream(conn.getInputStream()); + BufferedReader br = new BufferedReader(new InputStreamReader(is))) { + resp = br.lines().collect(Collectors.joining("\n")); + } + conn.disconnect(); + cachedToken = mapper.readValue(resp, JsonNode.class).get("access_token").asText(); + TokenCacheKey cacheKey = new TokenCacheKey(); + cacheKey.setKey(email + "_" + subdomain); + CacheConfig.getUserAuthoritiesTokenCache().put(cacheKey, cachedToken); + return cachedToken; + } + + public static String getDITokenUsingAuthorities( + SDMCredentials sdmCredentials, String email, String subdomain) throws IOException { + TokenCacheKey cacheKey = new TokenCacheKey(); + cacheKey.setKey(email + "_" + subdomain); + String cachedToken = CacheConfig.getUserAuthoritiesTokenCache().get(cacheKey); + if (cachedToken == null) { + cachedToken = getUserTokenFromAuthorities(email, subdomain, sdmCredentials); + } + return cachedToken; + } + + public static JsonObject getTokenFields(String token) { + String[] chunks = token.split("\\."); + java.util.Base64.Decoder decoder = java.util.Base64.getUrlDecoder(); + String payload = new String(decoder.decode(chunks[1])); + JsonElement jelement = new JsonParser().parse(payload); + return jelement.getAsJsonObject(); + } + + public static HttpClient getHttpClient( + ServiceBinding binding, + CdsProperties.ConnectionPool connectionPoolConfig, + String subdomain, + String type) { + if (!binding.getCredentials().isEmpty()) { + Map uaaCredentials = binding.getCredentials(); + Map uaa = (Map) uaaCredentials.get("uaa"); + ClientCredentials clientCredentials = + new ClientCredentials(uaa.get(CLIENT_ID).toString(), uaa.get(CLIENT_SECRET).toString()); + String baseTokenUrl = uaa.get(SDM_TOKEN_ENDPOINT).toString(); + if (subdomain != null && !subdomain.isEmpty()) { + String providersubdomain = + baseTokenUrl.substring(baseTokenUrl.indexOf("/") + 2, baseTokenUrl.indexOf(".")); + baseTokenUrl = baseTokenUrl.replace(providersubdomain, subdomain); + } + + DefaultHttpDestination destination; + if (type.equals("TOKEN_EXCHANGE")) { + destination = + OAuth2DestinationBuilder.forTargetUrl(uaaCredentials.get(SDM_URL).toString()) + .withTokenEndpoint(baseTokenUrl) + .withClient(clientCredentials, OnBehalfOf.NAMED_USER_CURRENT_TENANT) + .property("name", SDMConstants.SDM_TOKEN_EXCHANGE_DESTINATION) + .build(); + } else { + destination = + OAuth2DestinationBuilder.forTargetUrl(uaaCredentials.get(SDM_URL).toString()) + .withTokenEndpoint(baseTokenUrl) + .withClient(clientCredentials, OnBehalfOf.TECHNICAL_USER_CURRENT_TENANT) + .property("name", SDMConstants.SDM_TECHNICAL_CREDENTIALS_FLOW_DESTINATION) + .build(); + } + + DefaultHttpClientFactory.DefaultHttpClientFactoryBuilder builder = + DefaultHttpClientFactory.builder(); + builder.timeoutMilliseconds((int) connectionPoolConfig.getTimeout().toMillis()); + builder.maxConnectionsPerRoute(connectionPoolConfig.getMaxConnectionsPerRoute()); + builder.maxConnectionsTotal(connectionPoolConfig.getMaxConnections()); + DefaultHttpClientFactory factory = builder.build(); + + return factory.createHttpClient(destination); + } + return null; + } + + public static String getSubdomainFromToken(String token) { + JsonObject payloadObj = TokenHandler.getTokenFields(token); + JsonObject tenantDetails = payloadObj.get("ext_attr").getAsJsonObject(); + return tenantDetails.get("zdn").getAsString(); + } +} 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 new file mode 100644 index 00000000..b5187475 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandler.java @@ -0,0 +1,142 @@ +package com.sap.cds.sdm.handler.applicationservice; + +import com.sap.cds.CdsData; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.TokenHandler; +import com.sap.cds.sdm.model.CmisDocument; +import com.sap.cds.sdm.model.SDMCredentials; +import com.sap.cds.sdm.service.SDMService; +import com.sap.cds.sdm.utilities.SDMUtils; +import com.sap.cds.services.ServiceException; +import com.sap.cds.services.authentication.AuthenticationInfo; +import com.sap.cds.services.authentication.JwtTokenAuthenticationInfo; +import com.sap.cds.services.cds.ApplicationService; +import com.sap.cds.services.cds.CdsCreateEventContext; +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 java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@ServiceName(value = "*", type = ApplicationService.class) +public class SDMCreateAttachmentsHandler implements EventHandler { + + private final SDMService sdmService; + + public SDMCreateAttachmentsHandler(SDMService sdmService) { + this.sdmService = sdmService; + } + + @Before + @HandlerOrder(HandlerOrder.EARLY) + public void processBefore(CdsCreateEventContext context, List data) throws IOException { + updateName(context, data); + } + + public void updateName(CdsCreateEventContext context, List data) throws IOException { + Set duplicateFilenames = SDMUtils.isFileNameDuplicateInDrafts(data); + if (!duplicateFilenames.isEmpty()) { + handleDuplicateFilenames(context, duplicateFilenames); + } else { + List fileNameWithRestrictedCharacters = new ArrayList<>(); + List duplicateFileNameList = new ArrayList<>(); + for (Map entity : data) { + processEntity(context, entity, fileNameWithRestrictedCharacters, duplicateFileNameList); + } + handleWarnings(context, fileNameWithRestrictedCharacters, duplicateFileNameList); + } + } + + private void handleDuplicateFilenames( + CdsCreateEventContext context, Set duplicateFilenames) { + context + .getMessages() + .error( + String.format( + SDMConstants.DUPLICATE_FILE_IN_DRAFT_ERROR_MESSAGE, + String.join(", ", duplicateFilenames))); + } + + private void processEntity( + CdsCreateEventContext context, + Map entity, + List fileNameWithRestrictedCharacters, + List duplicateFileNameList) + throws IOException { + List> attachments = (List>) entity.get("attachments"); + if (attachments != null) { + for (Map attachment : attachments) { + processAttachment( + context, attachment, fileNameWithRestrictedCharacters, duplicateFileNameList); + } + } + } + + private void processAttachment( + CdsCreateEventContext context, + Map attachment, + List fileNameWithRestrictedCharacters, + List duplicateFileNameList) + throws IOException { + String filenameInRequest = (String) attachment.get("fileName"); + String objectId = (String) attachment.get("objectId"); + AuthenticationInfo authInfo = context.getAuthenticationInfo(); + JwtTokenAuthenticationInfo jwtTokenInfo = authInfo.as(JwtTokenAuthenticationInfo.class); + String jwtToken = jwtTokenInfo.getToken(); + SDMCredentials sdmCredentials = TokenHandler.getSDMCredentials(); + String fileNameInSDM = sdmService.getObject(jwtToken, objectId, sdmCredentials); + + if (fileNameInSDM != null && !fileNameInSDM.equals(filenameInRequest)) { + if (Boolean.TRUE.equals(SDMUtils.isRestrictedCharactersInName(filenameInRequest))) { + fileNameWithRestrictedCharacters.add(filenameInRequest); + attachment.replace("fileName", fileNameInSDM); + } else { + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setFileName(filenameInRequest); + cmisDocument.setObjectId(objectId); + int responseCode = sdmService.renameAttachments(jwtToken, sdmCredentials, cmisDocument); + switch (responseCode) { + case 403: + // SDM Roles for user are missing + throw new ServiceException(SDMConstants.SDM_MISSING_ROLES_EXCEPTION_MSG, null); + + case 409: + duplicateFileNameList.add(filenameInRequest); + attachment.replace("fileName", fileNameInSDM); + break; + + case 200: + case 201: + // Success cases, do nothing + break; + + default: + throw new ServiceException(SDMConstants.SDM_ROLES_ERROR_MESSAGE, null); + } + } + } + } + + private void handleWarnings( + CdsCreateEventContext context, + List fileNameWithRestrictedCharacters, + List duplicateFileNameList) { + if (!fileNameWithRestrictedCharacters.isEmpty()) { + context + .getMessages() + .warn(SDMConstants.nameConstraintMessage(fileNameWithRestrictedCharacters, "Rename")); + } + if (!duplicateFileNameList.isEmpty()) { + context + .getMessages() + .warn( + String.format( + SDMConstants.FILES_RENAME_WARNING_MESSAGE, + String.join(", ", duplicateFileNameList))); + } + } +} 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 new file mode 100644 index 00000000..110e5a2a --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandler.java @@ -0,0 +1,41 @@ +package com.sap.cds.sdm.handler.applicationservice; + +import com.sap.cds.ql.CQL; +import com.sap.cds.ql.Predicate; +import com.sap.cds.ql.cqn.CqnSelect; +import com.sap.cds.ql.cqn.Modifier; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.services.cds.ApplicationService; +import com.sap.cds.services.cds.CdsReadEventContext; +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 java.util.*; + +@ServiceName(value = "*", type = ApplicationService.class) +public class SDMReadAttachmentsHandler implements EventHandler { + + public SDMReadAttachmentsHandler() {} + + @Before + @HandlerOrder(HandlerOrder.DEFAULT) + public void processBefore(CdsReadEventContext context) { + String repositoryId = SDMConstants.REPOSITORY_ID; + if (context.getTarget().getQualifiedName().contains("attachments")) { + CqnSelect copy = + CQL.copy( + context.getCqn(), + new Modifier() { + @Override + public Predicate where(Predicate where) { + return CQL.and(where, CQL.get("repositoryId").eq(repositoryId)); + } + }); + context.setCqn(copy); + + } else { + context.setCqn(context.getCqn()); + } + } +} 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 new file mode 100644 index 00000000..123a688b --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandler.java @@ -0,0 +1,182 @@ +package com.sap.cds.sdm.handler.applicationservice; + +import static com.sap.cds.sdm.persistence.DBQuery.*; + +import com.sap.cds.CdsData; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.TokenHandler; +import com.sap.cds.sdm.model.CmisDocument; +import com.sap.cds.sdm.model.SDMCredentials; +import com.sap.cds.sdm.persistence.DBQuery; +import com.sap.cds.sdm.service.SDMService; +import com.sap.cds.sdm.utilities.SDMUtils; +import com.sap.cds.services.ServiceException; +import com.sap.cds.services.authentication.AuthenticationInfo; +import com.sap.cds.services.authentication.JwtTokenAuthenticationInfo; +import com.sap.cds.services.cds.ApplicationService; +import com.sap.cds.services.cds.CdsUpdateEventContext; +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.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +@ServiceName(value = "*", type = ApplicationService.class) +public class SDMUpdateAttachmentsHandler implements EventHandler { + private final PersistenceService persistenceService; + private final SDMService sdmService; + + public SDMUpdateAttachmentsHandler(PersistenceService persistenceService, SDMService sdmService) { + this.persistenceService = persistenceService; + this.sdmService = sdmService; + } + + @Before + @HandlerOrder(HandlerOrder.EARLY) + public void processBefore(CdsUpdateEventContext context, List data) throws IOException { + updateName(context, data); + } + + public void updateName(CdsUpdateEventContext context, List data) throws IOException { + Set duplicateFilenames = SDMUtils.isFileNameDuplicateInDrafts(data); + if (!duplicateFilenames.isEmpty()) { + context + .getMessages() + .error( + String.format( + SDMConstants.DUPLICATE_FILE_IN_DRAFT_ERROR_MESSAGE, + String.join(", ", duplicateFilenames))); + } else { + Optional attachmentEntity = + context.getModel().findEntity(context.getTarget().getQualifiedName() + ".attachments"); + renameDocument(attachmentEntity, context, data); + } + } + + private void renameDocument( + Optional attachmentEntity, CdsUpdateEventContext context, List data) + throws IOException { + List duplicateFileNameList = new ArrayList<>(); + List fileNameWithRestrictedCharacters = new ArrayList<>(); + for (Map entity : data) { + List> attachments = (List>) entity.get("attachments"); + if (attachments != null) { + processAttachments( + attachmentEntity, + context, + attachments, + duplicateFileNameList, + fileNameWithRestrictedCharacters); + } + } + handleWarnings(context, duplicateFileNameList, fileNameWithRestrictedCharacters); + } + + private void processAttachments( + Optional attachmentEntity, + CdsUpdateEventContext context, + List> attachments, + List duplicateFileNameList, + List fileNameWithRestrictedCharacters) + throws IOException { + Iterator> iterator = attachments.iterator(); + while (iterator.hasNext()) { + Map attachment = iterator.next(); + processAttachment( + attachmentEntity, + context, + attachment, + duplicateFileNameList, + fileNameWithRestrictedCharacters); + } + } + + public void processAttachment( + Optional attachmentEntity, + CdsUpdateEventContext context, + Map attachment, + List duplicateFileNameList, + List fileNameWithRestrictedCharacters) + throws IOException { + String id = (String) attachment.get("ID"); // Ensure appropriate cast to String + String filenameInRequest = (String) attachment.get("fileName"); + String objectId = (String) attachment.get("objectId"); + String fileNameInDB = + DBQuery.getAttachmentForID(attachmentEntity.get(), persistenceService, id); + String fileNameInSDM = getFileNameInSDM(context, fileNameInDB, objectId); + if (fileNameInSDM != null && !fileNameInSDM.equals(filenameInRequest)) { + if (Boolean.TRUE.equals(SDMUtils.isRestrictedCharactersInName(filenameInRequest))) { + fileNameWithRestrictedCharacters.add(filenameInRequest); + attachment.replace("fileName", fileNameInSDM); + return; + } + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setFileName(filenameInRequest); + cmisDocument.setObjectId(objectId); + int responseCode = + sdmService.renameAttachments( + context.getAuthenticationInfo().as(JwtTokenAuthenticationInfo.class).getToken(), + TokenHandler.getSDMCredentials(), + cmisDocument); + switch (responseCode) { + case 403: + // SDM Roles for user are missing + throw new ServiceException(SDMConstants.SDM_MISSING_ROLES_EXCEPTION_MSG, null); + + case 409: + duplicateFileNameList.add(filenameInRequest); + attachment.replace("fileName", fileNameInSDM); + break; + + case 200: + case 201: + // Success cases, do nothing + break; + + default: + throw new ServiceException(SDMConstants.SDM_ROLES_ERROR_MESSAGE, null); + } + } + } + + private String getFileNameInSDM( + CdsUpdateEventContext context, String fileNameInDB, String objectId) throws IOException { + AuthenticationInfo authInfo = context.getAuthenticationInfo(); + JwtTokenAuthenticationInfo jwtTokenInfo = authInfo.as(JwtTokenAuthenticationInfo.class); + String jwtToken = jwtTokenInfo.getToken(); + SDMCredentials sdmCredentials = TokenHandler.getSDMCredentials(); + if (Objects.isNull(fileNameInDB)) { + return sdmService.getObject(jwtToken, objectId, sdmCredentials); + } else { + return fileNameInDB; + } + } + + private void handleWarnings( + CdsUpdateEventContext context, + List duplicateFileNameList, + List fileNameWithRestrictedCharacters) { + if (!fileNameWithRestrictedCharacters.isEmpty()) { + context + .getMessages() + .warn(SDMConstants.nameConstraintMessage(fileNameWithRestrictedCharacters, "Rename")); + } + if (!duplicateFileNameList.isEmpty()) { + context + .getMessages() + .warn( + String.format( + SDMConstants.FILES_RENAME_WARNING_MESSAGE, + String.join(", ", duplicateFileNameList))); + } + } +} 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 new file mode 100644 index 00000000..43ac2a59 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/model/CmisDocument.java @@ -0,0 +1,25 @@ +package com.sap.cds.sdm.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.io.InputStream; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@JsonIgnoreProperties(ignoreUnknown = true) +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CmisDocument { + private String attachmentId; + private String objectId; + private String fileName; + private InputStream content; + private String parentId; + private String folderId; + private String repositoryId; + private String status; + private String mimeType; +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/model/SDMCredentials.java b/sdm/src/main/java/com/sap/cds/sdm/model/SDMCredentials.java new file mode 100644 index 00000000..b56877d1 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/model/SDMCredentials.java @@ -0,0 +1,23 @@ +package com.sap.cds.sdm.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@JsonIgnoreProperties(ignoreUnknown = true) +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SDMCredentials { + + private String url; + + private String baseTokenUrl; + + private String clientId; + + private String clientSecret; +} 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 new file mode 100644 index 00000000..dfa24bdd --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/persistence/DBQuery.java @@ -0,0 +1,99 @@ +package com.sap.cds.sdm.persistence; + +import com.sap.cds.Result; +import com.sap.cds.Row; +import com.sap.cds.ql.Select; +import com.sap.cds.ql.Update; +import com.sap.cds.ql.cqn.CqnSelect; +import com.sap.cds.ql.cqn.CqnUpdate; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.model.CmisDocument; +import com.sap.cds.services.persistence.PersistenceService; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DBQuery { + private DBQuery() { + // Doesn't do anything + } + + public static Result getAttachmentsForUPID( + CdsEntity attachmentEntity, PersistenceService persistenceService, String upID) { + CqnSelect q = + Select.from(attachmentEntity) + .columns("fileName", "ID", "IsActiveEntity", "folderId", "repositoryId") + .where(doc -> doc.get("up__ID").eq(upID)); + return persistenceService.run(q); + } + + public static String 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); + if (result.rowCount() == 0) { + return null; + } + return result.rowCount() == 0 ? null : result.list().get(0).get("fileName").toString(); + } + + public static void addAttachmentToDraft( + CdsEntity attachmentEntity, + PersistenceService persistenceService, + CmisDocument cmisDocument) { + String repositoryId = SDMConstants.REPOSITORY_ID; + Map updatedFields = new HashMap<>(); + updatedFields.put("objectId", cmisDocument.getObjectId()); + updatedFields.put("repositoryId", repositoryId); + updatedFields.put("folderId", cmisDocument.getFolderId()); + updatedFields.put("status", "Clean"); + + CqnUpdate updateQuery = + Update.entity(attachmentEntity) + .data(updatedFields) + .where(doc -> doc.get("ID").eq(cmisDocument.getAttachmentId())); + persistenceService.run(updateQuery); + } + + public static String getFolderIdForActiveEntity( + CdsEntity attachmentEntity, PersistenceService persistenceService, String upID) { + String res = null; + CqnSelect query = + Select.from(attachmentEntity) + .columns("folderId") + .where(doc -> doc.get("up__ID").eq(upID).and(doc.get("IsActiveEntity").eq(true))); + Result result = persistenceService.run(query); + + for (Map row : result.listOf(Map.class)) { + Object folderIdObj = row.get("folderId"); + if (folderIdObj != null) { + res = folderIdObj.toString(); + break; // Exit the loop after finding the first non-null folderId + } + } + return res; + } + + public static List getAttachmentsForFolder( + CdsEntity attachmentEntity, PersistenceService persistenceService, String folderId) { + List cmisDocuments = new ArrayList<>(); + CqnSelect q = + Select.from(attachmentEntity) + .columns("fileName", "IsActiveEntity", "ID", "folderId", "repositoryId", "objectId") + .where(doc -> doc.get("folderId").eq(folderId)); + Result result = persistenceService.run(q); + for (Row row : result.list()) { + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setFolderId(row.get("folderId").toString()); + cmisDocument.setRepositoryId(row.get("repositoryId").toString()); + cmisDocument.setFileName(row.get("fileName").toString()); + cmisDocument.setAttachmentId(row.get("ID").toString()); + cmisDocument.setObjectId(row.get("objectId").toString()); + cmisDocuments.add(cmisDocument); + } + return cmisDocuments; + } +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/RegisterService.java b/sdm/src/main/java/com/sap/cds/sdm/service/RegisterService.java new file mode 100644 index 00000000..01eadd91 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/service/RegisterService.java @@ -0,0 +1,7 @@ +package com.sap.cds.sdm.service; + +import com.sap.cds.services.Service; + +public interface RegisterService extends Service { + String SDM_NAME = "SDMAttachmentService$Default"; +} diff --git a/sdm/src/main/java/com/sap/cds/sdm/service/SDMAttachmentsService.java b/sdm/src/main/java/com/sap/cds/sdm/service/SDMAttachmentsService.java new file mode 100644 index 00000000..69e07c22 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/service/SDMAttachmentsService.java @@ -0,0 +1,87 @@ +package com.sap.cds.sdm.service; + +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; +import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.feature.attachments.service.model.service.AttachmentModificationResult; +import com.sap.cds.feature.attachments.service.model.service.CreateAttachmentInput; +import com.sap.cds.feature.attachments.service.model.service.MarkAsDeletedInput; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.DeletionUserInfo; +import com.sap.cds.services.ServiceDelegator; +import com.sap.cds.services.request.UserInfo; +import java.io.InputStream; +import java.time.Instant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SDMAttachmentsService extends ServiceDelegator + implements AttachmentService, RegisterService { + private static final Logger logger = LoggerFactory.getLogger(SDMAttachmentsService.class); + + public SDMAttachmentsService() { + super(SDM_NAME); + } + + @Override + public InputStream readAttachment(String contentId) { + logger.info("Reading attachment with document id: {}", contentId); + + var readContext = AttachmentReadEventContext.create(); + readContext.setContentId(contentId); + readContext.setData(MediaData.create()); + + emit(readContext); + + return readContext.getData().getContent(); + } + + @Override + public AttachmentModificationResult createAttachment(CreateAttachmentInput input) { + logger.info( + "Creating attachment for entity name: {}", input.attachmentEntity().getQualifiedName()); + var createContext = AttachmentCreateEventContext.create(); + createContext.setAttachmentIds(input.attachmentIds()); + createContext.setAttachmentEntity(input.attachmentEntity()); + var mediaData = MediaData.create(); + mediaData.setFileName(input.fileName()); + mediaData.setMimeType(input.mimeType()); + mediaData.setContent(input.content()); + createContext.setData(mediaData); + + emit(createContext); + + return new AttachmentModificationResult( + Boolean.TRUE.equals(createContext.getIsInternalStored()), + createContext.getContentId(), + createContext.getData().getStatus()); + } + + @Override + public void markAttachmentAsDeleted(MarkAsDeletedInput input) { + logger.info("Marking attachment as deleted for document id in SDM{}", input.contentId()); + + var deleteContext = AttachmentMarkAsDeletedEventContext.create(); + deleteContext.setContentId(input.contentId()); + deleteContext.setDeletionUserInfo(fillDeletionUserInfo(input.userInfo())); + + emit(deleteContext); + } + + @Override + public void restoreAttachment(Instant restoreTimestamp) { + logger.info("Restoring deleted attachment for timestamp: {}", restoreTimestamp); + var restoreContext = AttachmentRestoreEventContext.create(); + restoreContext.setRestoreTimestamp(restoreTimestamp); + + emit(restoreContext); + } + + private DeletionUserInfo fillDeletionUserInfo(UserInfo userInfo) { + var deletionUserInfo = DeletionUserInfo.create(); + deletionUserInfo.setName(userInfo.getName()); + return deletionUserInfo; + } +} 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 new file mode 100644 index 00000000..ebb0dd3c --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/service/SDMService.java @@ -0,0 +1,49 @@ +package com.sap.cds.sdm.service; + +import com.sap.cds.Result; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; +import com.sap.cds.sdm.model.CmisDocument; +import com.sap.cds.sdm.model.SDMCredentials; +import com.sap.cds.services.persistence.PersistenceService; +import java.io.IOException; +import org.json.JSONObject; + +public interface SDMService { + public JSONObject createDocument( + CmisDocument cmisDocument, SDMCredentials sdmCredentials, String jwtToken) throws IOException; + + public String createFolder( + String parentId, String repositoryId, SDMCredentials sdmCredentials, String jwtToken) + throws IOException; + + public String getFolderId( + Result result, PersistenceService persistenceService, String upID, String jwtToken) + throws IOException; + + public String getFolderIdByPath( + String parentId, String repositoryId, SDMCredentials sdmCredentials, String jwtToken) + throws IOException; + + public String checkRepositoryType(String jwtToken, String repositoryId) throws IOException; + + public JSONObject getRepositoryInfo(SDMCredentials sdmCredentials, String subdomain) + throws IOException; + + public Boolean isRepositoryVersioned(JSONObject repoInfo, String repositoryId) throws IOException; + + public int deleteDocument(String cmisaction, String objectId, String userEmail, String subdomain) + throws IOException; + + public void readDocument( + String objectId, + String jwtToken, + SDMCredentials sdmCredentials, + AttachmentReadEventContext context) + throws IOException; + + public int renameAttachments( + String jwtToken, SDMCredentials sdmCredentials, CmisDocument cmisDocument) throws IOException; + + public String getObject(String jwtToken, String objectId, SDMCredentials sdmCredentials) + 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 new file mode 100644 index 00000000..0c17f0d5 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/service/SDMServiceImpl.java @@ -0,0 +1,405 @@ +package com.sap.cds.sdm.service; + +import com.google.gson.JsonObject; +import com.sap.cds.Result; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; +import com.sap.cds.sdm.caching.CacheConfig; +import com.sap.cds.sdm.caching.RepoKey; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.TokenHandler; +import com.sap.cds.sdm.model.CmisDocument; +import com.sap.cds.sdm.model.SDMCredentials; +import com.sap.cds.services.ServiceException; +import com.sap.cds.services.environment.CdsProperties; +import com.sap.cds.services.persistence.PersistenceService; +import com.sap.cloud.environment.servicebinding.api.ServiceBinding; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.http.HttpEntity; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.json.JSONObject; + +public class SDMServiceImpl implements SDMService { + private final ServiceBinding binding; + private final CdsProperties.ConnectionPool connectionPool; + + public SDMServiceImpl(ServiceBinding binding, CdsProperties.ConnectionPool connectionPool) { + this.connectionPool = connectionPool; + this.binding = binding; + } + + @Override + public JSONObject createDocument( + CmisDocument cmisDocument, SDMCredentials sdmCredentials, String jwtToken) + throws IOException { + String subdomain = TokenHandler.getSubdomainFromToken(jwtToken); + var httpClient = + TokenHandler.getHttpClient(binding, connectionPool, subdomain, "TOKEN_EXCHANGE"); + Map finalResponse = new HashMap<>(); + String sdmUrl = sdmCredentials.getUrl() + "browser/" + cmisDocument.getRepositoryId() + "/root"; + + HttpPost uploadFile = new HttpPost(sdmUrl); + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.addBinaryBody( + "filename", + cmisDocument.getContent(), + ContentType.create(cmisDocument.getMimeType()), + cmisDocument.getFileName()); + // Add additional form fields + builder.addTextBody("cmisaction", "createDocument", ContentType.TEXT_PLAIN); + builder.addTextBody("objectId", cmisDocument.getFolderId(), ContentType.TEXT_PLAIN); + builder.addTextBody("propertyId[0]", "cmis:name", ContentType.TEXT_PLAIN); + builder.addTextBody("propertyValue[0]", cmisDocument.getFileName(), ContentType.TEXT_PLAIN); + builder.addTextBody("propertyId[1]", "cmis:objectTypeId", ContentType.TEXT_PLAIN); + builder.addTextBody("propertyValue[1]", "cmis:document", ContentType.TEXT_PLAIN); + builder.addTextBody("succinct", "true", ContentType.TEXT_PLAIN); + HttpEntity multipart = builder.build(); + uploadFile.setEntity(multipart); + executeHttpPost(httpClient, uploadFile, cmisDocument, finalResponse); + return new JSONObject(finalResponse); + } + + private void executeHttpPost( + HttpClient httpClient, + HttpPost uploadFile, + CmisDocument cmisDocument, + Map finalResponse) + throws ServiceException { + try (var response = (CloseableHttpResponse) httpClient.execute(uploadFile)) { + formResponse(cmisDocument, finalResponse, response); + } catch (IOException e) { + throw new ServiceException("Error in setting timeout", e.getMessage()); + } + } + + private void formResponse( + CmisDocument cmisDocument, + Map finalResponse, + CloseableHttpResponse response) { + String status = "success"; + String name = cmisDocument.getFileName(); + String id = cmisDocument.getAttachmentId(); + String objectId = ""; + String error = ""; + try { + String responseString = EntityUtils.toString(response.getEntity()); + JSONObject jsonResponse = new JSONObject(responseString); + int responseCode = response.getStatusLine().getStatusCode(); + if (responseCode == 201 || responseCode == 200) { + JSONObject succinctProperties = jsonResponse.getJSONObject("succinctProperties"); + status = "success"; + objectId = succinctProperties.getString("cmis:objectId"); + } else { + String message = jsonResponse.getString("message"); + if (responseCode == 409 + && "Malware Service Exception: Virus found in the file!".equals(message)) { + status = "virus"; + } else if (responseCode == 409) { + status = "duplicate"; + } else { + status = "fail"; + error = message; + } + } + // Construct the final response + finalResponse.put("name", name); + finalResponse.put("id", id); + finalResponse.put("status", status); + finalResponse.put("message", error); + if (!objectId.isEmpty()) { + finalResponse.put("objectId", objectId); + } + } catch (IOException e) { + throw new ServiceException(SDMConstants.getGenericError("upload")); + } + } + + @Override + public int renameAttachments( + String jwtToken, SDMCredentials sdmCredentials, CmisDocument cmisDocument) { + String repositoryId = SDMConstants.REPOSITORY_ID; + String subdomain = TokenHandler.getSubdomainFromToken(jwtToken); + var httpClient = + TokenHandler.getHttpClient(binding, connectionPool, subdomain, "TOKEN_EXCHANGE"); + String sdmUrl = sdmCredentials.getUrl() + "browser/" + repositoryId + "/root"; + String fileName = cmisDocument.getFileName(); + String objectId = cmisDocument.getObjectId(); + HttpPost renameRequest = new HttpPost(sdmUrl); + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + // Add additional form fields + builder.addTextBody("cmisaction", "update", ContentType.TEXT_PLAIN); + builder.addTextBody("propertyId[0]", "cmis:name", ContentType.TEXT_PLAIN); + builder.addTextBody("propertyValue[0]", fileName, ContentType.TEXT_PLAIN); + builder.addTextBody("objectId", objectId, ContentType.TEXT_PLAIN); + HttpEntity multipart = builder.build(); + renameRequest.setEntity(multipart); + try (var response = (CloseableHttpResponse) httpClient.execute(renameRequest)) { + return response.getStatusLine().getStatusCode(); + } catch (IOException e) { + throw new ServiceException(SDMConstants.COULD_NOT_RENAME_THE_ATTACHMENT, e); + } + } + + @Override + public String getObject(String jwtToken, String objectId, SDMCredentials sdmCredentials) + throws IOException { + String subdomain = TokenHandler.getSubdomainFromToken(jwtToken); + var httpClient = + TokenHandler.getHttpClient(binding, connectionPool, subdomain, "TOKEN_EXCHANGE"); + + String sdmUrl = + sdmCredentials.getUrl() + + "browser/" + + SDMConstants.REPOSITORY_ID + + "/root?cmisselector=object&objectId=" + + objectId + + "&succinct=true"; + + HttpGet getObjectRequest = new HttpGet(sdmUrl); + try (var response = (CloseableHttpResponse) httpClient.execute(getObjectRequest)) { + if (response.getStatusLine().getStatusCode() != 200) { + return null; + } + String responseString = EntityUtils.toString(response.getEntity()); + JSONObject jsonObject = new JSONObject(responseString); + JSONObject succinctProperties = jsonObject.getJSONObject("succinctProperties"); + return succinctProperties.getString("cmis:name"); + } catch (IOException e) { + throw new ServiceException(SDMConstants.ATTACHMENT_NOT_FOUND, e); + } + } + + @Override + public void readDocument( + String objectId, + String jwtToken, + SDMCredentials sdmCredentials, + AttachmentReadEventContext context) { + String repositoryId = SDMConstants.REPOSITORY_ID; + String subdomain = TokenHandler.getSubdomainFromToken(jwtToken); + var httpClient = + TokenHandler.getHttpClient(binding, connectionPool, subdomain, "TOKEN_EXCHANGE"); + + String sdmUrl = + sdmCredentials.getUrl() + + "browser/" + + repositoryId + + "/root?objectID=" + + objectId + + "&cmisselector=content"; + + HttpGet getContentRequest = new HttpGet(sdmUrl); + try (var response = (CloseableHttpResponse) httpClient.execute(getContentRequest)) { + int responseCode = response.getStatusLine().getStatusCode(); + if (responseCode != 200) { + response.close(); + if (responseCode == 404) { + throw new ServiceException(SDMConstants.FILE_NOT_FOUND_ERROR); + } + throw new ServiceException("Unexpected code"); + } + byte[] responseBody = EntityUtils.toByteArray(response.getEntity()); + try (InputStream inputStream = new ByteArrayInputStream(responseBody)) { + context.getData().setContent(inputStream); + } + } catch (Exception e) { + throw new ServiceException("Failed to set document stream in context"); + } + } + + @Override + public String getFolderId( + Result result, PersistenceService persistenceService, String upID, String token) { + + List> resultList = + result.listOf(Map.class).stream() + .map(map -> (Map) map) + .collect(Collectors.toList()); + + String folderId = null; + String repositoryId = null; + for (Map attachment : resultList) { + if (attachment.get("folderId") != null) { + folderId = attachment.get("folderId").toString(); + repositoryId = attachment.get("repositoryId").toString(); + } + } + String repoId = SDMConstants.REPOSITORY_ID; + // check if folderId exists for the repositoryId if not then make folderId null else continue + if (!repoId.equalsIgnoreCase(repositoryId)) { + folderId = null; + } + SDMCredentials sdmCredentials = TokenHandler.getSDMCredentials(); + + if (folderId == null) { + folderId = getFolderIdByPath(upID, SDMConstants.REPOSITORY_ID, sdmCredentials, token); + if (folderId == null) { + folderId = createFolder(upID, SDMConstants.REPOSITORY_ID, sdmCredentials, token); + JSONObject jsonObject = new JSONObject(folderId); + JSONObject succinctProperties = jsonObject.getJSONObject("succinctProperties"); + folderId = succinctProperties.getString("cmis:objectId"); + } + } + return folderId; + } + + @Override + public String getFolderIdByPath( + String parentId, String repositoryId, SDMCredentials sdmCredentials, String token) { + String subdomain = TokenHandler.getSubdomainFromToken(token); + var httpClient = + TokenHandler.getHttpClient(binding, connectionPool, subdomain, "TOKEN_EXCHANGE"); + String sdmUrl = + sdmCredentials.getUrl() + + "browser/" + + repositoryId + + "/root/" + + parentId + + "?cmisselector=object"; + HttpPost getFolderRequest = new HttpPost(sdmUrl); + try (var response = (CloseableHttpResponse) httpClient.execute(getFolderRequest)) { + int responseCode = response.getStatusLine().getStatusCode(); + if (responseCode == 200) { + return EntityUtils.toString(response.getEntity()); + } else if (responseCode == 403) { + throw new ServiceException(SDMConstants.USER_NOT_AUTHORISED_ERROR); + } + return null; + } catch (IOException e) { + throw new ServiceException(SDMConstants.getGenericError("upload")); + } + } + + @Override + public String createFolder( + String parentId, String repositoryId, SDMCredentials sdmCredentials, String jwtToken) { + String subdomain = TokenHandler.getSubdomainFromToken(jwtToken); + var httpClient = + TokenHandler.getHttpClient(binding, connectionPool, subdomain, "TOKEN_EXCHANGE"); + String sdmUrl = sdmCredentials.getUrl() + "browser/" + repositoryId + "/root"; + HttpPost createFolderRequest = new HttpPost(sdmUrl); + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + // Add additional form fields + builder.addTextBody("cmisaction", "createFolder", ContentType.TEXT_PLAIN); + builder.addTextBody("propertyId[0]", "cmis:name", ContentType.TEXT_PLAIN); + builder.addTextBody("propertyValue[0]", parentId, ContentType.TEXT_PLAIN); + builder.addTextBody("propertyId[1]", "cmis:objectTypeId", ContentType.TEXT_PLAIN); + builder.addTextBody("propertyValue[1]", "cmis:folder", ContentType.TEXT_PLAIN); + builder.addTextBody("succinct", "true", ContentType.TEXT_PLAIN); + HttpEntity multipart = builder.build(); + createFolderRequest.setEntity(multipart); + try (var response = (CloseableHttpResponse) httpClient.execute(createFolderRequest)) { + int responseCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity()); + if (responseCode == 201) return responseBody; + else if (responseCode == 403) { + throw new ServiceException(SDMConstants.USER_NOT_AUTHORISED_ERROR); + } else { + throw new ServiceException("Failed to create folder. " + responseBody); + } + } catch (IOException e) { + throw new ServiceException("Failed to create folder " + e.getMessage()); + } + } + + @Override + public String checkRepositoryType(String jwttoken, String repositoryId) { + RepoKey repoKey = new RepoKey(); + JsonObject payloadObj = TokenHandler.getTokenFields(jwttoken); + JsonObject tenantDetails = payloadObj.get("ext_attr").getAsJsonObject(); + String subdomain = tenantDetails.get("zdn").getAsString(); + repoKey.setSubdomain(subdomain); + repoKey.setRepoId(repositoryId); + String type = CacheConfig.getVersionedRepoCache().get(repoKey); + Boolean isVersioned; + if (type == null) { + SDMCredentials sdmCredentials = TokenHandler.getSDMCredentials(); + JSONObject repoInfo = getRepositoryInfo(sdmCredentials, subdomain); + isVersioned = isRepositoryVersioned(repoInfo, repositoryId); + } else { + isVersioned = "Versioned".equals(type); + } + + if (Boolean.TRUE.equals(isVersioned)) { + repoKey = new RepoKey(); + repoKey.setSubdomain(subdomain); + repoKey.setRepoId(repositoryId); + CacheConfig.getVersionedRepoCache().put(repoKey, "Versioned"); + return "Versioned"; + } else { + repoKey = new RepoKey(); + repoKey.setSubdomain(subdomain); + repoKey.setRepoId(repositoryId); + CacheConfig.getVersionedRepoCache().put(repoKey, "Non Versioned"); + return "Non Versioned"; + } + } + + public JSONObject getRepositoryInfo(SDMCredentials sdmCredentials, String subdomain) { + String repositoryId = SDMConstants.REPOSITORY_ID; + var httpClient = + TokenHandler.getHttpClient( + binding, connectionPool, subdomain, "TECHNICAL_CREDENTIALS_FLOW"); + + String getRepoInfoUrl = + sdmCredentials.getUrl() + "browser/" + repositoryId + "?cmisselector=repositoryInfo"; + HttpGet getRepoInfoRequest = new HttpGet(getRepoInfoUrl); + try (var response = (CloseableHttpResponse) httpClient.execute(getRepoInfoRequest)) { + if (response.getStatusLine().getStatusCode() != 200) + throw new ServiceException(SDMConstants.REPOSITORY_ERROR); + String responseString = EntityUtils.toString(response.getEntity()); + return new JSONObject(responseString); + } catch (IOException e) { + throw new ServiceException(SDMConstants.REPOSITORY_ERROR); + } + } + + public Boolean isRepositoryVersioned(JSONObject repoInfo, String repositoryId) { + repoInfo = repoInfo.getJSONObject(repositoryId); + JSONObject capabilities = repoInfo.getJSONObject("capabilities"); + String type = capabilities.getString("capabilityContentStreamUpdatability"); + if ("pwconly".equals(type)) { + type = "Versioned"; + } else { + type = "Non Versioned"; + } + + return "Versioned".equals(type); + } + + @Override + public int deleteDocument(String cmisaction, String objectId, String userEmail, String subdomain) + throws IOException { + SDMCredentials sdmCredentials = TokenHandler.getSDMCredentials(); + + HttpClient httpClient = HttpClients.createDefault(); + String accessToken = + TokenHandler.getDITokenUsingAuthorities(sdmCredentials, userEmail, subdomain); + String sdmUrl = sdmCredentials.getUrl() + "browser/" + SDMConstants.REPOSITORY_ID + "/root"; + HttpPost deleteDocumentRequest = new HttpPost(sdmUrl); + deleteDocumentRequest.setHeader("Authorization", "Bearer " + accessToken); + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + // Add additional form fields + builder.addTextBody("cmisaction", cmisaction, ContentType.TEXT_PLAIN); + builder.addTextBody("objectId", objectId, ContentType.TEXT_PLAIN); + HttpEntity multipart = builder.build(); + deleteDocumentRequest.setEntity(multipart); + try (var response = (CloseableHttpResponse) httpClient.execute(deleteDocumentRequest)) { + return response.getStatusLine().getStatusCode(); + } catch (IOException e) { + throw new ServiceException(SDMConstants.getGenericError("delete")); + } + } +} 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 new file mode 100644 index 00000000..fef10fe7 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandler.java @@ -0,0 +1,199 @@ +package com.sap.cds.sdm.service.handler; + +import static com.sap.cds.sdm.persistence.DBQuery.*; + +import com.sap.cds.Result; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; +import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.TokenHandler; +import com.sap.cds.sdm.model.CmisDocument; +import com.sap.cds.sdm.model.SDMCredentials; +import com.sap.cds.sdm.persistence.DBQuery; +import com.sap.cds.sdm.service.SDMService; +import com.sap.cds.sdm.utilities.SDMUtils; +import com.sap.cds.services.ServiceException; +import com.sap.cds.services.authentication.AuthenticationInfo; +import com.sap.cds.services.authentication.JwtTokenAuthenticationInfo; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.handler.annotations.On; +import com.sap.cds.services.handler.annotations.ServiceName; +import com.sap.cds.services.persistence.PersistenceService; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.json.JSONObject; + +@ServiceName(value = "*", type = AttachmentService.class) +public class SDMAttachmentsServiceHandler implements EventHandler { + private final PersistenceService persistenceService; + private final SDMService sdmService; + + public SDMAttachmentsServiceHandler( + PersistenceService persistenceService, SDMService sdmService) { + this.persistenceService = persistenceService; + this.sdmService = sdmService; + } + + @On(event = AttachmentService.EVENT_CREATE_ATTACHMENT) + public void createAttachment(AttachmentCreateEventContext context) throws IOException { + String subdomain = ""; + String repositoryId = SDMConstants.REPOSITORY_ID; + AuthenticationInfo authInfo = context.getAuthenticationInfo(); + JwtTokenAuthenticationInfo jwtTokenInfo = authInfo.as(JwtTokenAuthenticationInfo.class); + String jwtToken = jwtTokenInfo.getToken(); + String repocheck = sdmService.checkRepositoryType(jwtToken, repositoryId); + CmisDocument cmisDocument = new CmisDocument(); + if ("Versioned".equals(repocheck)) { + throw new ServiceException(SDMConstants.VERSIONED_REPO_ERROR); + } else { + Map attachmentIds = context.getAttachmentIds(); + String upID = (String) attachmentIds.get("up__ID"); + CdsModel model = context.getModel(); + Optional attachmentDraftEntity = + model.findEntity(context.getAttachmentEntity() + "_drafts"); + Result result = + DBQuery.getAttachmentsForUPID(attachmentDraftEntity.get(), persistenceService, upID); + if (!result.list().isEmpty()) { + MediaData data = context.getData(); + + String filename = data.getFileName(); + String fileid = (String) attachmentIds.get("ID"); + String mimeType = (String) data.get("mimeType"); + String errorMessageDI = ""; + boolean nameConstraint = SDMUtils.isRestrictedCharactersInName(filename); + if (nameConstraint) { + throw new ServiceException( + SDMConstants.nameConstraintMessage(Collections.singletonList(filename), "Upload")); + } + Boolean duplicate = duplicateCheck(filename, fileid, result); + if (Boolean.TRUE.equals(duplicate)) { + throw new ServiceException(SDMConstants.getDuplicateFilesError(filename)); + } else { + subdomain = TokenHandler.getSubdomainFromToken(jwtToken); + String folderId = sdmService.getFolderId(result, persistenceService, upID, jwtToken); + cmisDocument.setFileName(filename); + cmisDocument.setAttachmentId(fileid); + InputStream contentStream = (InputStream) data.get("content"); + cmisDocument.setContent(contentStream); + cmisDocument.setParentId((String) attachmentIds.get("up__ID")); + cmisDocument.setRepositoryId(repositoryId); + cmisDocument.setFolderId(folderId); + cmisDocument.setMimeType(mimeType); + SDMCredentials sdmCredentials = TokenHandler.getSDMCredentials(); + JSONObject createResult = + sdmService.createDocument(cmisDocument, sdmCredentials, jwtToken); + + if (createResult.get("status") == "duplicate") { + throw new ServiceException(SDMConstants.getDuplicateFilesError(filename)); + } else if (createResult.get("status") == "virus") { + throw new ServiceException(SDMConstants.getVirusFilesError(filename)); + } else if (createResult.get("status") == "fail") { + errorMessageDI = createResult.get("message").toString(); + throw new ServiceException(errorMessageDI); + } else { + cmisDocument.setObjectId(createResult.get("objectId").toString()); + addAttachmentToDraft(attachmentDraftEntity.get(), persistenceService, cmisDocument); + } + } + } + } + context.setContentId( + cmisDocument.getObjectId() + + ":" + + cmisDocument.getFolderId() + + ":" + + context.getAttachmentEntity() + + ":" + + subdomain); + context.getData().setStatus("Clean"); + context.getData().setContent(null); + context.setCompleted(); + } + + @On(event = AttachmentService.EVENT_MARK_ATTACHMENT_AS_DELETED) + public void markAttachmentAsDeleted(AttachmentMarkAsDeletedEventContext context) + throws IOException { + String[] contextValues = context.getContentId().split(":"); + if (contextValues.length > 0 && !(contextValues[0].equalsIgnoreCase("null"))) { + String objectId = contextValues[0]; + String folderId = contextValues[1]; + String userEmail = context.getDeletionUserInfo().getName(); + String entity = contextValues[2]; + String subdomain = contextValues[3]; + // check if only attachment exists against the folderId + Optional attachmentEntity = context.getModel().findEntity(entity); + List cmisDocuments = + DBQuery.getAttachmentsForFolder(attachmentEntity.get(), persistenceService, folderId); + if (cmisDocuments.isEmpty()) { + // deleteFolder API + sdmService.deleteDocument("deleteTree", folderId, userEmail, subdomain); + } else { + if (!isObjectIdPresent(cmisDocuments, objectId)) { + sdmService.deleteDocument("delete", objectId, userEmail, subdomain); + } + } + } + context.setCompleted(); + } + + @On(event = AttachmentService.EVENT_RESTORE_ATTACHMENT) + public void restoreAttachment(AttachmentRestoreEventContext context) { + context.setCompleted(); + } + + @On(event = AttachmentService.EVENT_READ_ATTACHMENT) + public void readAttachment(AttachmentReadEventContext context) throws IOException { + AuthenticationInfo authInfo = context.getAuthenticationInfo(); + JwtTokenAuthenticationInfo jwtTokenInfo = authInfo.as(JwtTokenAuthenticationInfo.class); + String jwtToken = jwtTokenInfo.getToken(); + String[] contentIdParts = context.getContentId().split(":"); + String objectId = contentIdParts[0]; + SDMCredentials sdmCredentials = TokenHandler.getSDMCredentials(); + try { + sdmService.readDocument(objectId, jwtToken, sdmCredentials, context); + } catch (Exception e) { + throw new ServiceException(e.getMessage()); + } + context.setCompleted(); + } + + public boolean duplicateCheck(String filename, String fileid, Result result) { + + List> resultList = + result.listOf(Map.class).stream() + .map(map -> (Map) map) + .collect(Collectors.toList()); + + Map duplicate = null; + for (Map attachment : resultList) { + String resultFileName = (String) attachment.get("fileName"); + String resultId = (String) attachment.get("ID"); + if (filename.equals(resultFileName) && !fileid.equals(resultId)) { + duplicate = attachment; + break; + } + } + + return duplicate != null; + } + + private boolean isObjectIdPresent(List documents, String objectId) { + for (CmisDocument doc : documents) { + if (objectId.equals(doc.getObjectId())) { + return true; + } + } + return false; + } +} 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 new file mode 100644 index 00000000..a2f0db74 --- /dev/null +++ b/sdm/src/main/java/com/sap/cds/sdm/utilities/SDMUtils.java @@ -0,0 +1,62 @@ +package com.sap.cds.sdm.utilities; + +import com.sap.cds.CdsData; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SDMUtils { + + private SDMUtils() { + // Doesn't do anything + } + + public static Set isFileNameDuplicateInDrafts(List data) { + Set uniqueFilenames = new HashSet<>(); + Set duplicateFilenames = new HashSet<>(); + for (Map entity : data) { + List> attachments = (List>) entity.get("attachments"); + if (attachments != null) { + Iterator> iterator = attachments.iterator(); + while (iterator.hasNext()) { + Map attachment = iterator.next(); + String filenameInRequest = (String) attachment.get("fileName"); + if (!uniqueFilenames.add(filenameInRequest)) { + duplicateFilenames.add(filenameInRequest); + } + } + } + } + return duplicateFilenames; + } + + public static List isFileNameContainsRestrictedCharaters(List data) { + List restrictedFilenames = new ArrayList(); + for (Map entity : data) { + List> attachments = (List>) entity.get("attachments"); + if (attachments != null) { + Iterator> iterator = attachments.iterator(); + while (iterator.hasNext()) { + Map attachment = iterator.next(); + String filenameInRequest = (String) attachment.get("fileName"); + if (isRestrictedCharactersInName(filenameInRequest)) { + restrictedFilenames.add(filenameInRequest); + } + } + } + } + return restrictedFilenames; + } + + public static boolean isRestrictedCharactersInName(String cmisName) { + String regex = "[/\\\\]"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(cmisName); + return matcher.find(); + } +} diff --git a/sdm/src/main/resources/META-INF/services/com.sap.cds.services.runtime.CdsRuntimeConfiguration b/sdm/src/main/resources/META-INF/services/com.sap.cds.services.runtime.CdsRuntimeConfiguration new file mode 100644 index 00000000..f67e8468 --- /dev/null +++ b/sdm/src/main/resources/META-INF/services/com.sap.cds.services.runtime.CdsRuntimeConfiguration @@ -0,0 +1 @@ +com.sap.cds.sdm.configuration.Registration \ No newline at end of file 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 new file mode 100644 index 00000000..b317a59b --- /dev/null +++ b/sdm/src/main/resources/cds/com.sap.cds/sdm/attachments.cds @@ -0,0 +1,38 @@ +namespace sap.attachments; + +using {sap.attachments.Attachments} from `com.sap.cds/cds-feature-attachments`; +extend aspect Attachments with { + folderId : String ; + repositoryId : String ; + objectId : String ; +} +annotate Attachments with @UI: { + HeaderInfo: { + $Type : 'UI.HeaderInfoType', + TypeName : '{i18n>Attachment}', + TypeNamePlural: '{i18n>Attachments}', + }, + 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%'}} + ] +} { + note @(title: '{i18n>Note}'); + fileName @(title: '{i18n>Filename}'); + modifiedAt @(odata.etag: null); + content + @Core.ContentDisposition: { Filename: fileName, Type: 'inline' } + @(title: '{i18n>Attachment}'); + folderId @UI.Hidden; + repositoryId @UI.Hidden ; + objectId @UI.Hidden ; + mimeType @UI.Hidden; + status @UI.Hidden; +} +annotate Attachments with @Common: {SideEffects #ContentChanged: { + SourceProperties: [content], + TargetProperties: ['status'] +}}{}; diff --git a/sdm/src/main/resources/cds/com.sap.cds/sdm/index.cds b/sdm/src/main/resources/cds/com.sap.cds/sdm/index.cds new file mode 100644 index 00000000..b2ce1e3f --- /dev/null +++ b/sdm/src/main/resources/cds/com.sap.cds/sdm/index.cds @@ -0,0 +1,2 @@ +using from`./attachments`; +using from '@sap/cds/srv/outbox'; diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java b/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java new file mode 100644 index 00000000..e1ab4fce --- /dev/null +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/Api.java @@ -0,0 +1,460 @@ +package integration.com.sap.cds.sdm; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.IOException; +import java.util.*; +import okhttp3.*; +import okio.ByteString; + +public class Api { + private final Map config; + private final OkHttpClient httpClient; + private static final ObjectMapper objectMapper = new ObjectMapper(); + private final String token; + + public Api(Map config) { + this.config = new HashMap<>(config); + this.httpClient = new OkHttpClient(); + this.token = this.config.get("Authorization"); + } + + public String createEntityDraft( + String appUrl, String serviceName, String entityName, String srvpath) { + MediaType mediaType = MediaType.parse("application/json"); + + // Creating the Entity (draft) + RequestBody body = + RequestBody.create( + mediaType, + "{\n \"title\": \"IntegrationTestEntity\",\n \"author\": {\n \"ID\": \"41cf82fb-94bf-4d62-9e45-fa25f959b5b0\",\n \"name\": \"Rishi\"\n }\n}"); + + Request request = + new Request.Builder() + .url("https://" + appUrl + "/odata/v4/" + serviceName + "/" + entityName) + .method("POST", body) + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", token) + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + System.out.println("Create entity failed. Error : " + response.body().string()); + throw new IOException("Could not create entity"); + } + Map responseMap = objectMapper.readValue(response.body().string(), Map.class); + return (String) responseMap.get("ID"); + } catch (IOException e) { + System.out.println("Could not create entity : " + e); + } + return ("Could not create entity"); + } + + public String editEntityDraft( + String appUrl, String serviceName, String entityName, String srvpath, String entityID) { + MediaType mediaType = MediaType.parse("application/json"); + Request request = + new Request.Builder() + .url( + "https://" + + appUrl + + "/odata/v4/" + + serviceName + + "/" + + entityName + + "(ID=" + + entityID + + ",IsActiveEntity=true)/" + + srvpath + + ".draftEdit") + .post(RequestBody.create("{\"PreserveChanges\":true}", mediaType)) + .addHeader("Authorization", token) + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (response.code() != 200) { + System.out.println("Edit entity failed. Error : " + response.body().string()); + throw new IOException("Could not edit entity"); + } + return "Entity in draft mode"; + } catch (IOException e) { + System.out.println("Could not edit entity : " + e); + } + return "Could not edit entity"; + } + + public String saveEntityDraft( + String appUrl, String serviceName, String entityName, String srvpath, String entityID) { + Request request = + new Request.Builder() + .url( + "https://" + + appUrl + + "/odata/v4/" + + serviceName + + "/" + + entityName + + "(ID=" + + entityID + + ",IsActiveEntity=false)/" + + srvpath + + ".draftPrepare") + .post( + RequestBody.create( + "{\"SideEffectsQualifier\":\"\"}", MediaType.parse("application/json"))) + .addHeader("Authorization", token) + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (response.code() != 200) { + System.out.println("Save entity failed. Error : " + response.body().string()); + throw new IOException("Could not save entity"); + } else { + request = + new Request.Builder() + .url( + "https://" + + appUrl + + "/odata/v4/" + + serviceName + + "/" + + entityName + + "(ID=" + + entityID + + ",IsActiveEntity=false)/" + + srvpath + + ".draftActivate") + .post(RequestBody.create("", null)) + .addHeader("Authorization", token) + .build(); + + try (Response draftResponse = httpClient.newCall(request).execute()) { + if (draftResponse.code() != 200) { + String draftResponseBodyString = draftResponse.body().string(); + System.out.println("Save entity failed. Error : " + draftResponseBodyString); + return (draftResponseBodyString); + } + return "Saved"; + } catch (IOException e) { + System.out.println("Could not save entity : " + e); + } + } + } catch (IOException e) { + System.out.println("Could not save entity : " + e); + } + + return "Could not save entity"; + } + + public String deleteEntity( + String appUrl, String serviceName, String entityName, String entityID) { + Request request = + new Request.Builder() + .url( + "https://" + + appUrl + + "/odata/v4/" + + serviceName + + "/" + + entityName + + "(ID=" + + entityID + + ",IsActiveEntity=true)") + .delete() + .addHeader("Authorization", token) + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + System.out.println("Delete entity failed. Error : " + response.body().string()); + throw new IOException("Could not delete entity"); + } + return "Entity Deleted"; + } catch (IOException e) { + System.out.println("Could not delete entity : " + e); + } + return ("Could not delete entity"); + } + + public String checkEntity(String appUrl, String serviceName, String entityName, String entityID) { + Request request = + new Request.Builder() + .url( + "https://" + + appUrl + + "/odata/v4/" + + serviceName + + "/" + + entityName + + "(ID=" + + entityID + + ",IsActiveEntity=true)") + .addHeader("Authorization", token) + .build(); + + try (Response checkResponse = httpClient.newCall(request).execute()) { + if (checkResponse.code() != 200) { + System.out.println("Verify entity failed. Error : " + checkResponse.body().string()); + throw new IOException("Entity doesn't exist"); + } else { + return "Entity exists"; + } + } catch (IOException e) { + System.out.println("Could not verify entity : " + e); + } + return ("Entity doesn't exist"); + } + + public List createAttachment( + String appUrl, + String serviceName, + String entityName, + String entityID, + String srvpath, + Map postData, + File file) + throws IOException { + String attachmentID; + String error = ""; + + // Creating empty attachments + String fileName = file.getName(); + + MediaType mediaType = MediaType.parse("application/json"); + RequestBody body = + RequestBody.create( + mediaType, ByteString.encodeUtf8("{\n \"fileName\" : \"" + fileName + "\"\n}")); + Request postRequest = + new Request.Builder() + .url( + "https://" + + appUrl + + "/odata/v4/" + + serviceName + + "/" + + entityName + + "(ID=" + + entityID + + ",IsActiveEntity=false)/attachments") + .method("POST", body) + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", token) + .build(); + + try (Response response = httpClient.newCall(postRequest).execute()) { + if (response.code() != 201) { + System.out.println("Create attachment failed. Error : " + response.body().string()); + throw new IOException("Could not create attachment"); + } + Map responseMap = objectMapper.readValue(response.body().string(), Map.class); + attachmentID = (String) responseMap.get("ID"); + + long startTime = System.nanoTime(); + // Upload file content into the empty attachment + RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/octet-stream")); + Request fileRequest = + new Request.Builder() + .url( + "https://" + + appUrl + + "/odata/v4/" + + serviceName + + "/Books_attachments(up__ID=" + + entityID + + ",ID=" + + attachmentID + + ",IsActiveEntity=false)/content") + .put(fileBody) + .addHeader("Authorization", token) + .build(); + + try (Response fileResponse = httpClient.newCall(fileRequest).execute()) { + if (fileResponse.code() != 204) { + String responseBodyString = fileResponse.body().string(); + System.out.println("Create attachment failed. Error : " + responseBodyString); + error = responseBodyString; + Request request = + new Request.Builder() + .url( + "https://" + + appUrl + + "/odata/v4/" + + serviceName + + "/Books_attachments(up__ID=" + + entityID + + ",ID=" + + attachmentID + + ",IsActiveEntity=false)") + .delete() + .addHeader("Authorization", token) + .build(); + + try (Response deleteResponse = httpClient.newCall(request).execute()) { + if (deleteResponse.code() != 204) { + System.out.println( + "Delete attachment failed. Error : " + deleteResponse.body().string()); + throw new IOException("Attachment was not created and its container was not deleted"); + } + List createResponse = new ArrayList<>(); + createResponse.add(error); + return createResponse; + } catch (IOException e) { + System.out.println( + "Attachment was not created and its container was not deleted : " + e); + } + } + long endTime = System.nanoTime(); // Record end time + double duration = (endTime - startTime) / 1_000_000_000.0; + System.out.println("Time taken to create(s) : " + duration); + List createResponse = new ArrayList<>(); + createResponse.add("Attachment created"); + createResponse.add(attachmentID); + return createResponse; + } catch (IOException e) { + System.out.println("Attachment was not created and its container was not deleted : " + e); + } + } catch (IOException e) { + System.out.println("Attachment was not created : " + e); + } + List createResponse = new ArrayList<>(); + createResponse.add("Attachment was not created"); + return createResponse; + } + + public String readAttachment( + String appUrl, String serviceName, String entityName, String entityID, String attachmentID) + throws IOException { + Request request = + new Request.Builder() + .url( + "https://" + + appUrl + + "/odata/v4/" + + serviceName + + "/" + + entityName + + "(ID=" + + entityID + + ",IsActiveEntity=true)/attachments(up__ID=" + + entityID + + ",ID=" + + attachmentID + + ",IsActiveEntity=true)/content") + .addHeader("Authorization", token) + .get() + .build(); + + try { + Response response = httpClient.newCall(request).execute(); + if (!response.isSuccessful()) { + System.out.println("Read attachment failed. Error : " + response.body().string()); + throw new IOException("Could not read attachment"); + } + return "OK"; + } catch (IOException e) { + System.out.println("Could not read attachment : " + e); + return "Could not read attachment"; + } + } + + public String readAttachmentDraft( + String appUrl, String serviceName, String entityName, String entityID, String attachmentID) + throws IOException { + Request request = + new Request.Builder() + .url( + "https://" + + appUrl + + "/odata/v4/" + + serviceName + + "/" + + entityName + + "(ID=" + + entityID + + ",IsActiveEntity=false)/attachments(up__ID=" + + entityID + + ",ID=" + + attachmentID + + ",IsActiveEntity=false)/content") + .addHeader("Authorization", token) + .get() + .build(); + + try { + Response response = httpClient.newCall(request).execute(); + if (!response.isSuccessful()) { + System.out.println("Read draft attachment failed. Error : " + response.body().string()); + throw new IOException("Could not read attachment"); + } + return "OK"; + } catch (IOException e) { + System.out.println("Could not read attachment : " + e); + return "Could not read attachment"; + } + } + + public String deleteAttachment( + String appUrl, String serviceName, String entityID, String attachmentID) { + Request request = + new Request.Builder() + .url( + "https://" + + appUrl + + "/odata/v4/" + + serviceName + + "/Books_attachments(up__ID=" + + entityID + + ",ID=" + + attachmentID + + ",IsActiveEntity=false)") + .delete() + .addHeader("Authorization", token) + .build(); + + try (Response deleteResponse = httpClient.newCall(request).execute()) { + if (deleteResponse.code() != 204) { + System.out.println("Delete attachment failed. Error : " + deleteResponse.body().string()); + throw new IOException("Attachment was not deleted"); + } + return "Deleted"; + } catch (IOException e) { + System.out.println("Attachment was not deleted : " + e); + return "Attachment was not deleted"; + } + } + + public String renameAttachment( + String appUrl, String serviceName, String entityID, String attachmentID, String name) { + MediaType mediaType = MediaType.parse("application/json"); + RequestBody body = + RequestBody.create( + mediaType, ByteString.encodeUtf8("{\n \"fileName\" : \"" + name + "\"\n}")); + Request request = + new Request.Builder() + .url( + "https://" + + appUrl + + "/odata/v4/" + + serviceName + + "/Books_attachments(up__ID=" + + entityID + + ",ID=" + + attachmentID + + ",IsActiveEntity=false)") + .method("PATCH", body) + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", token) + .build(); + + try (Response renameResponse = httpClient.newCall(request).execute()) { + if (renameResponse.code() != 200) { + System.out.println("Rename attachment failed. Error : " + renameResponse.body().string()); + throw new IOException("Attachment was not renamed"); + } + return "Renamed"; + } catch (IOException e) { + System.out.println("Attachment was not renamed : " + e); + return "Attachment was not renamed"; + } + } +} diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/AttachmentsSDMTest.java b/sdm/src/test/java/integration/com/sap/cds/sdm/AttachmentsSDMTest.java new file mode 100644 index 00000000..fa7dce6c --- /dev/null +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/AttachmentsSDMTest.java @@ -0,0 +1,479 @@ +package integration.com.sap.cds.sdm; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import okhttp3.*; +import org.junit.jupiter.api.*; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class AttachmentsSDMTest { + private static String token; + private static String entityID; + private static String entityID2; + private static String appUrl; + private static String authUrl; + private static String username; + private static String password; + private static String serviceName = "AdminService"; + private static String entityName = "Books"; + private static String srvpath = "AdminService"; + private static Api api; + private static String attachmentID1 = ""; + private static String attachmentID2 = ""; + private static String attachmentID3 = ""; + private static String attachmentID4 = ""; + + @BeforeAll + public static void setup() throws IOException { + // Define your clientId and clientSecret + Properties credentialsProperties = Credentials.getCredentials(); + String clientId = credentialsProperties.getProperty("clientID"); + String clientSecret = credentialsProperties.getProperty("clientSecret"); + appUrl = credentialsProperties.getProperty("appUrl"); + authUrl = credentialsProperties.getProperty("authUrl"); + username = credentialsProperties.getProperty("username"); + password = credentialsProperties.getProperty("password"); + + // Encode clientId:clientSecret to Base64 + String credentials = clientId + ":" + clientSecret; + String basicAuth = + "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + + OkHttpClient client = new OkHttpClient().newBuilder().build(); + MediaType mediaType = MediaType.parse("text/plain"); + RequestBody body = RequestBody.create(mediaType, ""); + Request request = + new Request.Builder() + .url( + authUrl + + "/oauth/token?grant_type=password&username=" + + username + + "&password=" + + password) + .method("POST", body) + .addHeader("Authorization", basicAuth) + .build(); + Response response = client.newCall(request).execute(); + if (response.code() != 200) { + System.out.println("Token generation failed. Response code: " + response.code()); + String errorBody = response.body().string(); + System.out.println("Error body: " + errorBody); + } + token = new ObjectMapper().readTree(response.body().string()).get("access_token").asText(); + response.close(); + Map config = new HashMap<>(); + config.put("Authorization", "Bearer " + token); + api = new Api(config); + } + + @Test + @Order(1) + public void testCreateEntityAndCheck() throws IOException { + System.out.println("Test (1) : Create entity and check if it exists"); + Boolean testStatus = false; + String response = api.createEntityDraft(appUrl, serviceName, entityName, srvpath); + if (response != "Could not create entity") { + entityID = response; + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response == "Saved") { + response = api.checkEntity(appUrl, serviceName, entityName, entityID); + if (response.equals("Entity exists")) { + testStatus = true; + } + } + } + if (!testStatus) { + fail("Could not create entity"); + } + } + + @Test + @Order(2) + public void testUpdateEmptyEntity() throws IOException { + System.out.println("Test (2) : Update an existing entity"); + Boolean testStatus = false; + String response = api.editEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response == "Entity in draft mode") { + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response == "Saved") { + response = api.checkEntity(appUrl, serviceName, entityName, entityID); + if (response.equals("Entity exists")) { + testStatus = true; + } + } + } + if (!testStatus) { + fail("Could not update entity"); + } + } + + @Test + @Order(3) + public void testUploadSingleAttachmentPDF() throws IOException { + System.out.println("Test (3) : Upload pdf"); + Boolean testStatus = false; + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource("sample.pdf").getFile()); + + Map postData = new HashMap<>(); + postData.put("up__ID", entityID); + postData.put("mimeType", "application/pdf"); + postData.put("createdAt", new Date().toString()); + postData.put("createdBy", "test@test.com"); + postData.put("modifiedBy", "test@test.com"); + + String response = api.editEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response == "Entity in draft mode") { + List createResponse = + api.createAttachment(appUrl, serviceName, entityName, entityID, srvpath, postData, file); + String check = createResponse.get(0); + if (check.equals("Attachment created")) { + attachmentID1 = createResponse.get(1); + response = + api.readAttachmentDraft(appUrl, serviceName, entityName, entityID, attachmentID1); + if (response.equals("OK")) { + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response.equals("Saved")) { + response = api.readAttachment(appUrl, serviceName, entityName, entityID, attachmentID1); + + if (response.equals("OK")) { + testStatus = true; + } + } + } + } + } + if (!testStatus) { + fail("Could not upload sample.pdf " + response); + } + } + + @Test + @Order(4) + public void testUploadSingleAttachmentTXT() throws IOException { + System.out.println("Test (4) : Upload txt"); + Boolean testStatus = false; + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource("sample.txt").getFile()); + + Map postData = new HashMap<>(); + postData.put("up__ID", entityID); + postData.put("mimeType", "application/txt"); + postData.put("createdAt", new Date().toString()); + postData.put("createdBy", "test@test.com"); + postData.put("modifiedBy", "test@test.com"); + + String response = api.editEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response == "Entity in draft mode") { + List createResponse = + api.createAttachment(appUrl, serviceName, entityName, entityID, srvpath, postData, file); + String check = createResponse.get(0); + if (check.equals("Attachment created")) { + attachmentID2 = createResponse.get(1); + response = + api.readAttachmentDraft(appUrl, serviceName, entityName, entityID, attachmentID2); + if (response.equals("OK")) { + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response.equals("Saved")) { + response = api.readAttachment(appUrl, serviceName, entityName, entityID, attachmentID2); + if (response.equals("OK")) { + testStatus = true; + } + } + } + } + } + if (!testStatus) { + fail("Could not upload sample.txt"); + } + } + + @Test + @Order(5) + public void testUploadSingleAttachmentEXE() throws IOException { + System.out.println("Test (5) : Upload exe"); + Boolean testStatus = false; + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource("sample.exe").getFile()); + + Map postData = new HashMap<>(); + postData.put("up__ID", entityID); + postData.put("mimeType", "application/exe"); + postData.put("createdAt", new Date().toString()); + postData.put("createdBy", "test@test.com"); + postData.put("modifiedBy", "test@test.com"); + + String response = api.editEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response == "Entity in draft mode") { + List createResponse = + api.createAttachment(appUrl, serviceName, entityName, entityID, srvpath, postData, file); + String check = createResponse.get(0); + if (check.equals("Attachment created")) { + attachmentID3 = createResponse.get(1); + response = + api.readAttachmentDraft(appUrl, serviceName, entityName, entityID, attachmentID3); + if (response.equals("OK")) { + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response.equals("Saved")) { + response = api.readAttachment(appUrl, serviceName, entityName, entityID, attachmentID3); + if (response.equals("OK")) { + testStatus = true; + } + } + } + } + } + if (!testStatus) { + fail("Could not create sample.exe"); + } + } + + @Test + @Order(6) + public void testUploadSingleAttachmentPDFDuplicate() throws IOException { + System.out.println("Test (6) : Upload duplicate pdf"); + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource("sample.pdf").getFile()); + Boolean testStatus = false; + + Map postData = new HashMap<>(); + postData.put("up__ID", entityID); + postData.put("mimeType", "application/pdf"); + postData.put("createdAt", new Date().toString()); + postData.put("createdBy", "test@test.com"); + postData.put("modifiedBy", "test@test.com"); + + String response = api.editEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response == "Entity in draft mode") { + List createResponse = + api.createAttachment(appUrl, serviceName, entityName, entityID, srvpath, postData, file); + String check = createResponse.get(0); + if (check.equals("Attachment created")) { + testStatus = false; + } else { + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response.equals("Saved")) { + String expectedJson = + "{\"error\":{\"code\":\"500\",\"message\":\"sample.pdf already exists.\"}}"; + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode actualJsonNode = objectMapper.readTree(check); + JsonNode expectedJsonNode = objectMapper.readTree(expectedJson); + if (expectedJsonNode.equals(actualJsonNode)) { + testStatus = true; + } + } + } + } + if (!testStatus) { + fail("Attachment was created"); + } + } + + @Test + @Order(7) + public void testUploadSingleAttachmentPDFDuplicateDifferentEntity() throws IOException { + System.out.println("Test (7) : Upload duplicate pdf in different entity"); + Boolean testStatus = false; + String response = api.createEntityDraft(appUrl, serviceName, entityName, srvpath); + if (response != "Could not create entity") { + entityID2 = response; + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID2); + if (response == "Saved") { + response = api.checkEntity(appUrl, serviceName, entityName, entityID2); + if (response.equals("Entity exists")) { + testStatus = true; + } + } + } + if (!testStatus) { + fail("Could not create entity"); + } + + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource("sample.pdf").getFile()); + + Map postData = new HashMap<>(); + postData.put("up__ID", entityID2); + postData.put("mimeType", "application/pdf"); + postData.put("createdAt", new Date().toString()); + postData.put("createdBy", "test@test.com"); + postData.put("modifiedBy", "test@test.com"); + + response = api.editEntityDraft(appUrl, serviceName, entityName, srvpath, entityID2); + if (response == "Entity in draft mode") { + List createResponse = + api.createAttachment(appUrl, serviceName, entityName, entityID2, srvpath, postData, file); + String check = createResponse.get(0); + if (check.equals("Attachment created")) { + attachmentID4 = createResponse.get(1); + response = + api.readAttachmentDraft(appUrl, serviceName, entityName, entityID2, attachmentID4); + if (response.equals("OK")) { + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID2); + if (response.equals("Saved")) { + response = + api.readAttachment(appUrl, serviceName, entityName, entityID2, attachmentID4); + + if (response.equals("OK")) { + testStatus = true; + } + } + } + } + } + if (!testStatus) { + fail("Could not upload sample.pdf " + response); + } + } + + @Test + @Order(8) + public void testRenameSingleAttachment() throws IOException { + System.out.println("Test (8) : Rename single attachment"); + Boolean testStatus = false; + String response = api.editEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + String name = "sample123"; + if (response == "Entity in draft mode") { + response = api.renameAttachment(appUrl, serviceName, entityID, attachmentID1, name); + if (response.equals("Renamed")) { + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response.equals("Saved")) { + testStatus = true; + } + } else { + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + } + } + if (!testStatus) { + fail("Attachment was not renamed"); + } + } + + @Test + @Order(9) + public void testRenameMultipleAttachments() throws IOException { + System.out.println("Test (9) : Rename multiple attachments"); + Boolean testStatus = false; + String response = api.editEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + String name1 = "sample1234"; + String name2 = "sample12345"; + if (response == "Entity in draft mode") { + String response1 = api.renameAttachment(appUrl, serviceName, entityID, attachmentID2, name1); + String response2 = api.renameAttachment(appUrl, serviceName, entityID, attachmentID3, name2); + if (response1.equals("Renamed") && response2.equals("Renamed")) { + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response.equals("Saved")) { + testStatus = true; + } + } else { + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + } + } + if (!testStatus) { + fail("Attachment was not renamed"); + } + } + + @Test + @Order(10) + public void testRenameSingleAttachmentDuplicate() throws IOException { + System.out.println("Test (10) : Rename single attachment duplicate"); + Boolean testStatus = false; + String response = api.editEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + String name = "sample123"; + String name2 = "sample123456"; + if (response == "Entity in draft mode") { + response = api.renameAttachment(appUrl, serviceName, entityID, attachmentID3, name); + if (response.equals("Renamed")) { + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + String expected = + "{\"error\":{\"code\":\"400\",\"message\":\"The file(s) sample123 have been added " + + "multiple times. Please rename and try again.\"}}"; + if (response.equals(expected)) { + response = api.renameAttachment(appUrl, serviceName, entityID, attachmentID3, name2); + if (response.equals("Renamed")) { + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response.equals("Saved")) { + testStatus = true; + } + } + } + } else { + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + } + } + if (!testStatus) { + fail("Attachment was renamed"); + } + } + + @Test + @Order(11) + public void testDeleteSingleAttachment() throws IOException { + System.out.println("Test (11) : Delete single attachment"); + Boolean testStatus = false; + String response = api.editEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response == "Entity in draft mode") { + response = api.deleteAttachment(appUrl, serviceName, entityID, attachmentID1); + if (response == "Deleted") { + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response == "Saved") { + response = api.readAttachment(appUrl, serviceName, entityName, entityID, attachmentID1); + if (response.equals("Could not read attachment")) { + testStatus = true; + } + } + } + } + if (!testStatus) { + fail("Could not delete attachment"); + } + } + + @Test + @Order(12) + public void testDeleteMultipleAttachments() throws IOException { + System.out.println("Test (12) : Delete multiple attachments"); + Boolean testStatus = false; + String response = api.editEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response == "Entity in draft mode") { + String response1 = api.deleteAttachment(appUrl, serviceName, entityID, attachmentID2); + String response2 = api.deleteAttachment(appUrl, serviceName, entityID, attachmentID3); + if (response1 == "Deleted" && response2 == "Deleted") { + response = api.saveEntityDraft(appUrl, serviceName, entityName, srvpath, entityID); + if (response == "Saved") { + response1 = api.readAttachment(appUrl, serviceName, entityName, entityID, attachmentID2); + response2 = api.readAttachment(appUrl, serviceName, entityName, entityID, attachmentID3); + if (response1.equals("Could not read attachment") + && response2.equals("Could not read attachment")) { + testStatus = true; + } + } + } + } + if (!testStatus) { + fail("Could not delete attachment"); + } + } + + @Test + @Order(13) + public void testDeleteEntity() throws IOException { + System.out.println("Test (13) : Delete entity"); + Boolean testStatus = false; + String response = api.deleteEntity(appUrl, serviceName, entityName, entityID); + String response2 = api.deleteEntity(appUrl, serviceName, entityName, entityID2); + if (response == "Entity Deleted" && response2 == "Entity Deleted") { + testStatus = true; + } + if (!testStatus) { + fail("Could not delete entity"); + } + } +} diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/Credentials.java b/sdm/src/test/java/integration/com/sap/cds/sdm/Credentials.java new file mode 100644 index 00000000..741fdfb9 --- /dev/null +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/Credentials.java @@ -0,0 +1,17 @@ +package integration.com.sap.cds.sdm; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; + +public class Credentials { + public static Properties getCredentials() { + Properties properties = new Properties(); + try (FileInputStream input = new FileInputStream("src/test/resources/credentials.properties")) { + properties.load(input); + } catch (IOException ex) { + ex.printStackTrace(); + } + return properties; + } +} diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/configuration/RegistrationTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/configuration/RegistrationTest.java new file mode 100644 index 00000000..b5ad4df0 --- /dev/null +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/configuration/RegistrationTest.java @@ -0,0 +1,101 @@ +package unit.com.sap.cds.sdm.configuration; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.Mockito.*; + +import com.sap.cds.feature.attachments.service.AttachmentService; +import com.sap.cds.sdm.configuration.Registration; +import com.sap.cds.sdm.service.handler.SDMAttachmentsServiceHandler; +import com.sap.cds.services.Service; +import com.sap.cds.services.ServiceCatalog; +import com.sap.cds.services.environment.CdsEnvironment; +import com.sap.cds.services.handler.EventHandler; +import com.sap.cds.services.outbox.OutboxService; +import com.sap.cds.services.persistence.PersistenceService; +import com.sap.cds.services.runtime.CdsRuntime; +import com.sap.cds.services.runtime.CdsRuntimeConfigurer; +import com.sap.cloud.environment.servicebinding.api.ServiceBinding; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +public class RegistrationTest { + private Registration registration; + private CdsRuntimeConfigurer configurer; + private ServiceCatalog serviceCatalog; + private PersistenceService persistenceService; + private AttachmentService attachmentService; + private OutboxService outboxService; + private ArgumentCaptor serviceArgumentCaptor; + private ArgumentCaptor handlerArgumentCaptor; + + @BeforeEach + void setup() { + registration = new Registration(); + configurer = mock(CdsRuntimeConfigurer.class); + CdsRuntime cdsRuntime = mock(CdsRuntime.class); + when(configurer.getCdsRuntime()).thenReturn(cdsRuntime); + serviceCatalog = mock(ServiceCatalog.class); + when(cdsRuntime.getServiceCatalog()).thenReturn(serviceCatalog); + CdsEnvironment environment = mock(CdsEnvironment.class); + when(cdsRuntime.getEnvironment()).thenReturn(environment); + ServiceBinding binding1 = mock(ServiceBinding.class); + ServiceBinding binding2 = mock(ServiceBinding.class); + ServiceBinding binding3 = mock(ServiceBinding.class); + + // Create a stream of bindings to be returned by environment.getServiceBindings() + Stream bindingsStream = Stream.of(binding1, binding2, binding3); + when(environment.getProperty("cds.attachments.sdm.http.timeout", Integer.class, 1200)) + .thenReturn(1800); + when(environment.getProperty("cds.attachments.sdm.http.maxConnections", Integer.class, 100)) + .thenReturn(200); + + persistenceService = mock(PersistenceService.class); + attachmentService = mock(AttachmentService.class); + outboxService = mock(OutboxService.class); + serviceArgumentCaptor = ArgumentCaptor.forClass(Service.class); + handlerArgumentCaptor = ArgumentCaptor.forClass(EventHandler.class); + } + + @Test + void serviceIsRegistered() { + registration.services(configurer); + + verify(configurer).service(serviceArgumentCaptor.capture()); + var services = serviceArgumentCaptor.getAllValues(); + assertThat(services).hasSize(1); + String prefix = "test"; + + // Perform the property reading + + var attachmentServiceFound = + services.stream().anyMatch(service -> service instanceof AttachmentService); + + assertThat(attachmentServiceFound).isTrue(); + } + + @Test + void handlersAreRegistered() { + when(serviceCatalog.getService(PersistenceService.class, PersistenceService.DEFAULT_NAME)) + .thenReturn(persistenceService); + when(serviceCatalog.getService(OutboxService.class, OutboxService.PERSISTENT_UNORDERED_NAME)) + .thenReturn(outboxService); + + registration.eventHandlers(configurer); + + var handlerSize = 4; + verify(configurer, times(handlerSize)).eventHandler(handlerArgumentCaptor.capture()); + var handlers = handlerArgumentCaptor.getAllValues(); + assertThat(handlers).hasSize(handlerSize); + isHandlerForClassIncluded(handlers, SDMAttachmentsServiceHandler.class); + } + + private void isHandlerForClassIncluded( + List handlers, Class includedClass) { + var isHandlerIncluded = + handlers.stream().anyMatch(handler -> handler.getClass() == includedClass); + assertThat(isHandlerIncluded).isTrue(); + } +} diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/handler/TokenHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/handler/TokenHandlerTest.java new file mode 100644 index 00000000..91c7c5aa --- /dev/null +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/handler/TokenHandlerTest.java @@ -0,0 +1,229 @@ +package unit.com.sap.cds.sdm.handler; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import com.sap.cds.sdm.caching.CacheConfig; +import com.sap.cds.sdm.handler.TokenHandler; +import com.sap.cds.sdm.model.SDMCredentials; +import com.sap.cds.services.environment.CdsProperties; +import com.sap.cloud.environment.servicebinding.api.DefaultServiceBindingAccessor; +import com.sap.cloud.environment.servicebinding.api.ServiceBinding; +import com.sap.cloud.environment.servicebinding.api.ServiceBindingAccessor; +import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpClientFactory; +import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; +import com.sap.cloud.security.xsuaa.client.OAuth2ServiceException; +import java.io.*; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.HttpURLConnection; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.*; +import org.apache.http.client.HttpClient; +import org.ehcache.Cache; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; + +public class TokenHandlerTest { + private String email = "email-value"; + private String subdomain = "subdomain-value"; + private static final String SDM_TOKEN_ENDPOINT = "url"; + private static final String SDM_URL = "uri"; + + private static final String CLIENT_ID = "clientid"; + private static final String CLIENT_SECRET = "clientsecret"; + @Mock private ServiceBinding binding; + + @Mock private CdsProperties.ConnectionPool connectionPoolConfig; + + @Mock private DefaultHttpClientFactory factory; + + @Mock private HttpClient httpClient; + + private Map uaaCredentials; + private Map uaa; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + + uaaCredentials = new HashMap<>(); + uaa = new HashMap<>(); + + uaa.put(CLIENT_ID, "test-client-id"); + uaa.put(CLIENT_SECRET, "test-client-secret"); + uaa.put(SDM_TOKEN_ENDPOINT, "https://test-token-url.com"); + + uaaCredentials.put("uaa", uaa); + uaaCredentials.put(SDM_URL, "https://example.com"); + + when(binding.getCredentials()).thenReturn(uaaCredentials); + when(connectionPoolConfig.getTimeout()).thenReturn(Duration.ofMillis(1000)); + when(connectionPoolConfig.getMaxConnectionsPerRoute()).thenReturn(10); + when(connectionPoolConfig.getMaxConnections()).thenReturn(100); + + // Instantiate and mock the factory + when(factory.createHttpClient(any(DefaultHttpDestination.class))).thenReturn(httpClient); + } + + @Test + public void testGetHttpClientForTokenExchange() { + HttpClient client = + TokenHandler.getHttpClient(binding, connectionPoolConfig, "subdomain", "TOKEN_EXCHANGE"); + + assertNotNull(client); + } + + @Test + public void testGetHttpClientForTechnicalUser() { + HttpClient client = + TokenHandler.getHttpClient(binding, connectionPoolConfig, "subdomain", "TECHNICAL_USER"); + + assertNotNull(client); + } + + @Test + public void testGetHttpClientWithNullSubdomain() { + HttpClient client = + TokenHandler.getHttpClient(binding, connectionPoolConfig, null, "TOKEN_EXCHANGE"); + + assertNotNull(client); + } + + @Test + public void testGetHttpClientWithEmptySubdomain() { + HttpClient client = + TokenHandler.getHttpClient(binding, connectionPoolConfig, "", "TOKEN_EXCHANGE"); + + assertNotNull(client); + } + + @Test + public void testGetDITokenFromAuthoritiesNoCache() throws IOException { + SDMCredentials mockSdmCredentials = Mockito.mock(SDMCredentials.class); + when(mockSdmCredentials.getClientId()).thenReturn("mockClientId"); + when(mockSdmCredentials.getClientSecret()).thenReturn("mockClientSecret"); + when(mockSdmCredentials.getBaseTokenUrl()).thenReturn("https://example.com"); + + Cache mockCache = Mockito.mock(Cache.class); + when(mockCache.get(any())).thenReturn(null); // Cache is empty + + try (MockedStatic cacheConfigMockedStatic = + Mockito.mockStatic(CacheConfig.class)) { + + cacheConfigMockedStatic.when(CacheConfig::getUserAuthoritiesTokenCache).thenReturn(mockCache); + HttpURLConnection mockConn = Mockito.mock(HttpURLConnection.class); + doNothing().when(mockConn).setRequestMethod("POST"); + ByteArrayOutputStream mockOutputStream = new ByteArrayOutputStream(); + // when(mockConn.getOutputStream()).thenReturn(new DataOutputStream(mockOutputStream)); + doReturn(new DataOutputStream(mockOutputStream)).when(mockConn).getOutputStream(); + doThrow(new IOException()).when(mockConn).getInputStream(); + Exception exception = + assertThrows( + IOException.class, + () -> { + TokenHandler.getDITokenUsingAuthorities(mockSdmCredentials, email, subdomain); + }); + + assertEquals("subdomain-value.com", exception.getMessage()); + } + } + + @Test + public void testGetSDMCredentials() { + ServiceBindingAccessor mockAccessor = Mockito.mock(ServiceBindingAccessor.class); + try (MockedStatic accessorMockedStatic = + Mockito.mockStatic(DefaultServiceBindingAccessor.class)) { + accessorMockedStatic + .when(DefaultServiceBindingAccessor::getInstance) + .thenReturn(mockAccessor); + + ServiceBinding mockServiceBinding = Mockito.mock(ServiceBinding.class); + + Map mockCredentials = new HashMap<>(); + Map mockUaa = new HashMap<>(); + mockUaa.put("url", "https://mock.uaa.url"); + mockUaa.put("clientid", "mockClientId"); + mockUaa.put("clientsecret", "mockClientSecret"); + mockCredentials.put("uaa", mockUaa); + mockCredentials.put("uri", "https://mock.service.url"); + + Mockito.when(mockServiceBinding.getServiceName()).thenReturn(Optional.of("sdm")); + Mockito.when(mockServiceBinding.getCredentials()).thenReturn(mockCredentials); + + List mockServiceBindings = Collections.singletonList(mockServiceBinding); + Mockito.when(mockAccessor.getServiceBindings()).thenReturn(mockServiceBindings); + + SDMCredentials result = TokenHandler.getSDMCredentials(); + + assertNotNull(result); + assertEquals("https://mock.uaa.url", result.getBaseTokenUrl()); + assertEquals("https://mock.service.url", result.getUrl()); + assertEquals("mockClientId", result.getClientId()); + assertEquals("mockClientSecret", result.getClientSecret()); + } + } + + @Test + public void testGetDITokenFromAuthorities() throws IOException { + SDMCredentials mockSdmCredentials = Mockito.mock(SDMCredentials.class); + when(mockSdmCredentials.getClientId()).thenReturn("mockClientId"); + when(mockSdmCredentials.getClientSecret()).thenReturn("mockClientSecret"); + when(mockSdmCredentials.getBaseTokenUrl()).thenReturn("https://mock.url"); + + try (MockedStatic cacheConfigMockedStatic = + Mockito.mockStatic(CacheConfig.class)) { + + Cache mockCache = Mockito.mock(Cache.class); + Mockito.when(mockCache.get(any())).thenReturn("cachedToken"); // Cache is empty + cacheConfigMockedStatic.when(CacheConfig::getUserAuthoritiesTokenCache).thenReturn(mockCache); + String result = TokenHandler.getDITokenUsingAuthorities(mockSdmCredentials, email, subdomain); + assertEquals("cachedToken", result); // Adjust based on the expected result + } catch (OAuth2ServiceException e) { + throw new RuntimeException(e); + } + } + + @Test + void testPrivateConstructor() { + // Use reflection to access the private constructor + Constructor constructor = null; + try { + constructor = TokenHandler.class.getDeclaredConstructor(); + constructor.setAccessible(true); + assertThrows(InvocationTargetException.class, constructor::newInstance); + } catch (NoSuchMethodException e) { + fail("Exception occurred during test: " + e.getMessage()); + } + } + + @Test + void testToString() { + byte[] input = "Hello, World!".getBytes(StandardCharsets.UTF_8); + String expected = new String(input, StandardCharsets.UTF_8); + String actual = TokenHandler.toString(input); + assertEquals(expected, actual); + } + + @Test + void testToStringWithNullInput() { + assertThrows(NullPointerException.class, () -> TokenHandler.toString(null)); + } + + @Test + public void testGetSubdomainFromToken() { + String token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTY4MzQxODI4MCwiZXhwIjoxNjg1OTQ0MjgwLCJleHRfYXR0ciI6eyJ6ZG4iOiJ0ZW5hbnQifX0.efgtgCjF7bxG2kEgYbkTObovuZN5YQP5t7yr9aPKntk"; + // Performing the actual test + String result = TokenHandler.getSubdomainFromToken(token); + + // Asserting the expected result + assertEquals("tenant", result); + } +} 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 new file mode 100644 index 00000000..4ba0f8e8 --- /dev/null +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMCreateAttachmentsHandlerTest.java @@ -0,0 +1,473 @@ +package unit.com.sap.cds.sdm.handler.applicationservice; + +import static com.sap.cds.sdm.utilities.SDMUtils.isFileNameDuplicateInDrafts; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import com.sap.cds.CdsData; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.TokenHandler; +import com.sap.cds.sdm.handler.applicationservice.SDMCreateAttachmentsHandler; +import com.sap.cds.sdm.model.CmisDocument; +import com.sap.cds.sdm.model.SDMCredentials; +import com.sap.cds.sdm.service.SDMService; +import com.sap.cds.sdm.service.SDMServiceImpl; +import com.sap.cds.sdm.utilities.SDMUtils; +import com.sap.cds.services.ServiceException; +import com.sap.cds.services.authentication.AuthenticationInfo; +import com.sap.cds.services.authentication.JwtTokenAuthenticationInfo; +import com.sap.cds.services.cds.CdsCreateEventContext; +import com.sap.cds.services.messages.Messages; +import com.sap.cds.services.persistence.PersistenceService; +import java.io.IOException; +import java.util.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; + +public class SDMCreateAttachmentsHandlerTest { + + @Mock private PersistenceService persistenceService; + @Mock private CdsCreateEventContext context; + @Mock private AuthenticationInfo authInfo; + @Mock private JwtTokenAuthenticationInfo jwtTokenInfo; + @Mock private SDMCredentials mockCredentials; + @Mock private Messages messages; + private SDMService sdmService; + + private SDMCreateAttachmentsHandler handler; // Use Spy to allow partial mocking + + private MockedStatic tokenHandlerMockedStatic; + private MockedStatic sdmUtilsMockedStatic; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + // Set up static mocking for `TokenHandler.getSDMCredentials` + sdmService = mock(SDMServiceImpl.class); + tokenHandlerMockedStatic = mockStatic(TokenHandler.class); + tokenHandlerMockedStatic.when(TokenHandler::getSDMCredentials).thenReturn(mockCredentials); + handler = spy(new SDMCreateAttachmentsHandler(sdmService)); + } + + @AfterEach + public void tearDown() { + if (tokenHandlerMockedStatic != null) { + tokenHandlerMockedStatic.close(); + } + if (sdmUtilsMockedStatic != null) { + sdmUtilsMockedStatic.close(); + } + } + + @Test + public void testProcessBefore() throws IOException { + List data = new ArrayList<>(); + doNothing().when(handler).updateName(any(CdsCreateEventContext.class), anyList()); + + handler.processBefore(context, data); + + verify(handler, times(1)).updateName(context, data); + } + + @Test + public void testRenameWithDuplicateFilenames() throws IOException { + List data = new ArrayList<>(); + Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); + when(context.getMessages()).thenReturn(messages); + sdmUtilsMockedStatic = mockStatic(SDMUtils.class); + sdmUtilsMockedStatic + .when(() -> isFileNameDuplicateInDrafts(data)) + .thenReturn(duplicateFilenames); + + handler.updateName(context, data); + + verify(messages, times(1)) + .error( + "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); + } + + @Test + public void testRenameWithNoDuplicateFilenames() throws IOException { + List data = new ArrayList<>(); + handler.updateName(context, data); + + verify(messages, never()).error(anyString()); + } + + @Test + public void testRenameWithNoAttachments() throws IOException { + List data = new ArrayList<>(); + CdsData mockCdsData = mock(CdsData.class); + when(mockCdsData.get("attachments")).thenReturn(null); + data.add(mockCdsData); + + handler.updateName(context, data); + + verify(sdmService, never()) + .renameAttachments(anyString(), any(SDMCredentials.class), any(CmisDocument.class)); + } + + @Test + public void testRenameWithoutFileInSDM() throws IOException { + List data = new ArrayList<>(); + CdsData mockCdsData = mock(CdsData.class); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = new HashMap<>(); + attachment.put("fileName", "file1.txt"); + attachment.put("url", "objectId"); + attachments.add(attachment); + entity.put("attachments", attachments); + when(mockCdsData.get("attachments")).thenReturn(attachments); + data.add(mockCdsData); + + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + // Mock the static TokenHandler + when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + + // Mock the SDM service responses + when(sdmService.getObject(any(), any(), any())) + .thenReturn(null); // Mock with same file name in SDM to not trigger renaming + + handler.updateName(context, data); + + verify(sdmService, never()) + .renameAttachments(anyString(), any(SDMCredentials.class), any(CmisDocument.class)); + } + + @Test + public void testRenameWithSameFileNameInSDM() throws IOException { + List data = new ArrayList<>(); + CdsData mockCdsData = mock(CdsData.class); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = new HashMap<>(); + attachment.put("fileName", "file1.txt"); + attachment.put("url", "objectId"); + attachments.add(attachment); + entity.put("attachments", attachments); + when(mockCdsData.get("attachments")).thenReturn(attachments); + data.add(mockCdsData); + + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + // Mock the static TokenHandler + when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + + // Mock the SDM service responses + when(sdmService.getObject(any(), any(), any())) + .thenReturn("file1.txt"); // Mock with same file name in SDM to not trigger renaming + + handler.updateName(context, data); + + verify(sdmService, never()) + .renameAttachments(anyString(), any(SDMCredentials.class), any(CmisDocument.class)); + } + + @Test + public void testRenameWithConflictResponseCode() throws IOException { + // Mock the data structure to simulate the attachments + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = spy(new HashMap<>()); + attachment.put("fileName", "file1.txt"); + attachment.put("url", "objectId"); + attachment.put("ID", "test-id"); // assuming there's an ID field + attachments.add(attachment); + entity.put("attachments", attachments); + CdsData mockCdsData = mock(CdsData.class); + when(mockCdsData.get("attachments")).thenReturn(attachments); + data.add(mockCdsData); + + // Mock the authentication context + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + + // Mock the static TokenHandler + when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + + // Mock the SDM service responses + when(sdmService.getObject(any(), any(), any())) + .thenReturn("file-sdm.txt"); // Mock a different file name in SDM to trigger renaming + when(sdmService.renameAttachments( + anyString(), any(SDMCredentials.class), any(CmisDocument.class))) + .thenReturn(409); // Mock conflict response code + + // Mock the returned messages + when(context.getMessages()).thenReturn(messages); + + // Execute the method under test + handler.updateName(context, data); + + // Verify the attachment's file name was attempted to be replaced with "file-sdm.txt" + verify(attachment).replace("fileName", "file-sdm.txt"); + + // Verify that a warning message was added to the context + verify(messages, times(1)) + .warn("The following files could not be renamed as they already exist:\nfile1.txt\n"); + } + + @Test + public void testCreateAttachmentWithNoSDMRoles() throws IOException { + // Mock the data structure to simulate the attachments + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = spy(new HashMap<>()); + attachment.put("fileName", "file1.txt"); + attachment.put("url", "objectId"); + attachment.put("ID", "test-id"); // assuming there's an ID field + attachments.add(attachment); + entity.put("attachments", attachments); + CdsData mockCdsData = mock(CdsData.class); + when(mockCdsData.get("attachments")).thenReturn(attachments); + data.add(mockCdsData); + + // Mock the authentication context + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + + // Mock the static TokenHandler + when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + + // Mock the SDM service responses + when(sdmService.getObject(any(), any(), any())) + .thenReturn("file-sdm.txt"); // Mock a different file name in SDM to trigger renaming + when(sdmService.renameAttachments( + anyString(), any(SDMCredentials.class), any(CmisDocument.class))) + .thenReturn(403); // Mock conflict response code + + when(sdmService.renameAttachments( + anyString(), any(SDMCredentials.class), any(CmisDocument.class))) + .thenReturn(403); // Mock conflict response code + + ServiceException exception = + assertThrows( + ServiceException.class, + () -> { + handler.updateName(context, data); + }); + + assertEquals(SDMConstants.SDM_MISSING_ROLES_EXCEPTION_MSG, exception.getMessage()); + } + + @Test + public void testCreateAttachmentWith500Error() throws IOException { + // Mock the data structure to simulate the attachments + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = spy(new HashMap<>()); + attachment.put("fileName", "file1.txt"); + attachment.put("url", "objectId"); + attachment.put("ID", "test-id"); // assuming there's an ID field + attachments.add(attachment); + entity.put("attachments", attachments); + CdsData mockCdsData = mock(CdsData.class); + when(mockCdsData.get("attachments")).thenReturn(attachments); + data.add(mockCdsData); + + // Mock the authentication context + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + + // Mock the static TokenHandler + when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + + // Mock the SDM service responses + when(sdmService.getObject(any(), any(), any())) + .thenReturn("file-sdm.txt"); // Mock a different file name in SDM to trigger renaming + when(sdmService.renameAttachments( + anyString(), any(SDMCredentials.class), any(CmisDocument.class))) + .thenReturn(500); // Mock conflict response code + ServiceException exception = + assertThrows( + ServiceException.class, + () -> { + handler.updateName(context, data); + }); + + assertEquals(SDMConstants.SDM_ROLES_ERROR_MESSAGE, exception.getMessage()); + } + + @Test + public void testRenameWith200ResponseCode() throws IOException { + // Mock the data structure to simulate the attachments + System.out.println("testRenameWithConflictResponseCode"); + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = spy(new HashMap<>()); + attachment.put("fileName", "file1.txt"); + attachment.put("url", "objectId"); + attachment.put("ID", "test-id"); // assuming there's an ID field + attachments.add(attachment); + entity.put("attachments", attachments); + CdsData mockCdsData = mock(CdsData.class); + when(mockCdsData.get("attachments")).thenReturn(attachments); + data.add(mockCdsData); + + // Mock the authentication context + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + + // Mock the static TokenHandler + when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + + // Mock the SDM service responses + when(sdmService.getObject(any(), any(), any())) + .thenReturn("file-sdm.txt"); // Mock a different file name in SDM to trigger renaming + when(sdmService.renameAttachments( + anyString(), any(SDMCredentials.class), any(CmisDocument.class))) + .thenReturn(200); // Mock conflict response code + + // Mock the returned messages + when(context.getMessages()).thenReturn(messages); + + // Execute the method under test + handler.updateName(context, data); + + verify(attachment, never()).replace("fileName", "file-sdm.txt"); + + // Verify that a warning message was added to the context + verify(messages, times(0)) + .warn("The following files could not be renamed as they already exist:\nfile1.txt\n"); + } + + @Test + public void testRenameWithRestrictedCharacters() throws IOException { + // Prepare the test data with restricted characters in filenames + List data = prepareMockAttachmentData("file1.txt", "file/2.txt", "file\\3.txt"); + List fileNameWithRestrictedChars = new ArrayList<>(); + fileNameWithRestrictedChars.add("file/2.txt"); + fileNameWithRestrictedChars.add("file\\3.txt"); + + // Mock the CdsEntity and setup context + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + when(context.getMessages()).thenReturn(messages); + + // Mock SDMUtils to simulate restricted characters + MockedStatic sdmUtilsMockedStatic = mockStatic(SDMUtils.class); + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) + .thenAnswer( + invocation -> { + String filename = invocation.getArgument(0); + return filename.contains("/") || filename.contains("\\"); + }); + + // Mock the SDM service object retrieval + when(sdmService.getObject(anyString(), anyString(), any())).thenReturn("file-in-sdm"); + + // Ensure renameAttachments behaves as expected + when(sdmService.renameAttachments(anyString(), any(), any(CmisDocument.class))) + .thenReturn(200); // or a desired response code + + // Act + handler.updateName(context, data); + + // Verify warning message about restricted characters + verify(messages, times(1)) + .warn(SDMConstants.nameConstraintMessage(fileNameWithRestrictedChars, "Rename")); + + // Verify the filenames with restricted characters are replaced in attachments + for (CdsData cdsData : data) { + List> attachments = + (List>) cdsData.get("attachments"); + for (Map attachment : attachments) { + String filename = (String) attachment.get("fileName"); + if (filename.equals("file/2.txt") || filename.equals("file\\3.txt")) { + // Ensure the filename is replaced + verify(attachment).replace("fileName", "file-in-sdm"); + } + } + } + + // Close the mocked static method + sdmUtilsMockedStatic.close(); + } + + @Test + public void testWarnOnRestrictedCharacters() throws IOException { + // Prepare the sample data with restricted characters + List data = prepareMockAttachmentData("file1.txt", "file/2.txt", "file3\\abc.txt"); + List fileNameWithRestrictedChars = new ArrayList<>(); + fileNameWithRestrictedChars.add("file/2.txt"); + fileNameWithRestrictedChars.add("file3\\abc.txt"); + + // Mock context and related authentication methods + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + + when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + when(sdmService.getObject(anyString(), anyString(), any())).thenReturn("file-in-sdm"); + + // Mock message handling + when(context.getMessages()).thenReturn(messages); + + // Mock SDMUtils restricted character check + try (MockedStatic sdmUtilsMockedStatic = mockStatic(SDMUtils.class)) { + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) + .thenAnswer( + invocation -> { + String filename = invocation.getArgument(0); + return filename.contains("/") || filename.contains("\\"); + }); + + // Mock renameAttachments implementation to avoid ServiceExceptions for testing + when(sdmService.renameAttachments(any(String.class), any(), any(CmisDocument.class))) + .thenReturn(200); // assuming successful rename + + // Act by invoking the handler updateName method with the context and data + handler.updateName(context, data); + + // Verify the warning for restricted filenames is correctly handled + verify(messages, times(1)) + .warn(SDMConstants.nameConstraintMessage(fileNameWithRestrictedChars, "Rename")); + + // Ensure no error messages are appearing unexpectedly + verify(messages, never()).error(anyString()); + } + } + + private List prepareMockAttachmentData(String... fileNames) { + List data = new ArrayList<>(); + for (String fileName : fileNames) { + CdsData cdsData = mock(CdsData.class); + List> attachments = new ArrayList<>(); + Map attachment = new HashMap<>(); + attachment.put("ID", UUID.randomUUID().toString()); + attachment.put("fileName", fileName); + attachment.put("objectId", "objectId-" + UUID.randomUUID()); + attachments.add(attachment); + when(cdsData.get("attachments")).thenReturn(attachments); + data.add(cdsData); + } + return data; + } +} diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandlerTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandlerTest.java new file mode 100644 index 00000000..6d082f1f --- /dev/null +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMReadAttachmentsHandlerTest.java @@ -0,0 +1,75 @@ +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.verify; +import static org.mockito.Mockito.when; + +import com.sap.cds.ql.Select; +import com.sap.cds.ql.cqn.CqnSelect; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.applicationservice.SDMReadAttachmentsHandler; +import com.sap.cds.services.cds.CdsReadEventContext; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class SDMReadAttachmentsHandlerTest { + + @Mock private CdsEntity cdsEntity; + + @Mock private CdsReadEventContext context; + + @Mock private CdsModel model; + + @Mock private CqnSelect cqnSelect; + + @InjectMocks private SDMReadAttachmentsHandler sdmReadAttachmentsHandler; + + private static final String REPOSITORY_ID_KEY = SDMConstants.REPOSITORY_ID; + + @Test + void testModifyCqnForAttachmentsEntity_Success() { + // Arrange + String targetEntity = "attachments"; + CqnSelect select = + Select.from(cdsEntity).where(doc -> doc.get("repositoryId").eq(REPOSITORY_ID_KEY)); + when(context.getTarget()).thenReturn(cdsEntity); + when(context.getCqn()).thenReturn(select); + when(cdsEntity.getQualifiedName()).thenReturn(targetEntity); + + // Act + sdmReadAttachmentsHandler.processBefore(context); // Refers to the method you provided + + // Verify the modified where clause + // Predicate whereClause = modifiedCqnSelect.where(); + + // Add assertions to validate the modification in `where` clause + assertNotNull(select.where().isPresent()); + assertTrue(select.where().toString().contains("repositoryId")); + } + + @Test + void testModifyCqnForNonAttachmentsEntity() { + // Arrange + String targetEntity = "nonAttachments"; + CqnSelect select = + Select.from(cdsEntity).where(doc -> doc.get("repositoryId").eq(REPOSITORY_ID_KEY)); + when(context.getTarget()).thenReturn(cdsEntity); + when(context.getCqn()).thenReturn(select); + when(cdsEntity.getQualifiedName()).thenReturn(targetEntity); + when(context.getTarget().getQualifiedName()).thenReturn(targetEntity); + + // Act + sdmReadAttachmentsHandler.processBefore(context); // Refers to the method you provided + + // Assert + verify(context) + .setCqn(select); // Ensure that the original CqnSelect is set without modification + } +} 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 new file mode 100644 index 00000000..cd2d498f --- /dev/null +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/handler/applicationservice/SDMUpdateAttachmentsHandlerTest.java @@ -0,0 +1,522 @@ +package unit.com.sap.cds.sdm.handler.applicationservice; + +import static com.sap.cds.sdm.persistence.DBQuery.getAttachmentForID; +import static com.sap.cds.sdm.utilities.SDMUtils.isFileNameDuplicateInDrafts; +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import com.sap.cds.CdsData; +import com.sap.cds.Result; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.TokenHandler; +import com.sap.cds.sdm.handler.applicationservice.SDMUpdateAttachmentsHandler; +import com.sap.cds.sdm.model.CmisDocument; +import com.sap.cds.sdm.model.SDMCredentials; +import com.sap.cds.sdm.persistence.DBQuery; +import com.sap.cds.sdm.service.SDMService; +import com.sap.cds.sdm.service.SDMServiceImpl; +import com.sap.cds.sdm.utilities.SDMUtils; +import com.sap.cds.services.ServiceException; +import com.sap.cds.services.authentication.AuthenticationInfo; +import com.sap.cds.services.authentication.JwtTokenAuthenticationInfo; +import com.sap.cds.services.cds.CdsUpdateEventContext; +import com.sap.cds.services.messages.Messages; +import com.sap.cds.services.persistence.PersistenceService; +import java.io.IOException; +import java.util.*; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class SDMUpdateAttachmentsHandlerTest { + + @Mock private PersistenceService persistenceService; + @Mock private CdsUpdateEventContext context; + @Mock private AuthenticationInfo authInfo; + @Mock private JwtTokenAuthenticationInfo jwtTokenInfo; + @Mock private SDMCredentials mockCredentials; + @Mock private Messages messages; + @Mock private Result result; + @Mock private CdsEntity cdsEntity; + @Mock private CdsModel model; + private SDMService sdmService; + + private SDMUpdateAttachmentsHandler handler; + + private MockedStatic tokenHandlerMockedStatic; + private MockedStatic dbQueryMockedStatic; + private MockedStatic sdmUtilsMockedStatic; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + sdmService = mock(SDMServiceImpl.class); + tokenHandlerMockedStatic = mockStatic(TokenHandler.class); + tokenHandlerMockedStatic.when(TokenHandler::getSDMCredentials).thenReturn(mockCredentials); + handler = spy(new SDMUpdateAttachmentsHandler(persistenceService, sdmService)); + } + + @AfterEach + public void tearDown() { + if (tokenHandlerMockedStatic != null) { + tokenHandlerMockedStatic.close(); + } + if (dbQueryMockedStatic != null) { + dbQueryMockedStatic.close(); + } + if (sdmUtilsMockedStatic != null) { + sdmUtilsMockedStatic.close(); + } + } + + @Test + public void testProcessBeforeCallsRename() throws IOException { + List data = new ArrayList<>(); + doNothing().when(handler).updateName(any(CdsUpdateEventContext.class), anyList()); + handler.processBefore(context, data); + verify(handler, times(1)).updateName(context, data); + } + + @Test + public void testRenameWithDuplicateFilenames() throws IOException { + List data = new ArrayList<>(); + Set duplicateFilenames = new HashSet<>(Arrays.asList("file1.txt", "file2.txt")); + when(context.getMessages()).thenReturn(messages); + sdmUtilsMockedStatic = mockStatic(SDMUtils.class); + sdmUtilsMockedStatic + .when(() -> isFileNameDuplicateInDrafts(data)) + .thenReturn(duplicateFilenames); + + handler.updateName(context, data); + + verify(messages, times(1)) + .error( + "The file(s) file1.txt, file2.txt have been added multiple times. Please rename and try again."); + } + + @Test + public void testRenameWithUniqueFilenames() throws IOException { + List data = prepareMockAttachmentData("file1.txt"); + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(model.findEntity("some.qualified.Name.attachments")) + .thenReturn(Optional.of(attachmentDraftEntity)); + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + + dbQueryMockedStatic = mockStatic(DBQuery.class); + dbQueryMockedStatic + .when( + () -> + getAttachmentForID( + any(CdsEntity.class), any(PersistenceService.class), anyString())) + .thenReturn("file1.txt"); + + handler.updateName(context, data); + verify(sdmService, never()) + .renameAttachments(anyString(), any(SDMCredentials.class), any(CmisDocument.class)); + } + + @Test + public void testRenameWithConflictResponseCode() throws IOException { + // Mock the data structure to simulate the attachments + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = spy(new HashMap<>()); + attachment.put("fileName", "file1.txt"); + attachment.put("url", "objectId"); + attachment.put("ID", "test-id"); // assuming there's an ID field + attachments.add(attachment); + entity.put("attachments", attachments); + CdsData mockCdsData = mock(CdsData.class); + when(mockCdsData.get("attachments")).thenReturn(attachments); + data.add(mockCdsData); + + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(model.findEntity("some.qualified.Name.attachments")) + .thenReturn(Optional.of(attachmentDraftEntity)); + + // Mock the authentication context + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + + // Mock the static TokenHandler + when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + + // Mock the SDM service responses + dbQueryMockedStatic = mockStatic(DBQuery.class); + dbQueryMockedStatic + .when( + () -> + getAttachmentForID( + any(CdsEntity.class), any(PersistenceService.class), anyString())) + .thenReturn("file123.txt"); // Mock a different file name in SDM to trigger renaming + + when(sdmService.renameAttachments( + anyString(), any(SDMCredentials.class), any(CmisDocument.class))) + .thenReturn(409); // Mock conflict response code + + // Mock the returned messages + when(context.getMessages()).thenReturn(messages); + + // Execute the method under test + handler.updateName(context, data); + + // Verify the attachment's file name was attempted to be replaced with "file-sdm.txt" + verify(attachment).put("fileName", "file1.txt"); + + // Verify that a warning message was added to the context + verify(messages, times(1)) + .warn("The following files could not be renamed as they already exist:\nfile1.txt\n"); + } + + @Test + public void testRenameWithNoSDMRoles() throws IOException { + // Mock the data structure to simulate the attachments + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = spy(new HashMap<>()); + attachment.put("fileName", "file1.txt"); + attachment.put("url", "objectId"); + attachment.put("ID", "test-id"); // assuming there's an ID field + attachments.add(attachment); + entity.put("attachments", attachments); + CdsData mockCdsData = mock(CdsData.class); + when(mockCdsData.get("attachments")).thenReturn(attachments); + data.add(mockCdsData); + + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(model.findEntity("some.qualified.Name.attachments")) + .thenReturn(Optional.of(attachmentDraftEntity)); + + // Mock the authentication context + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + + // Mock the static TokenHandler + when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + + // Mock the SDM service responses + dbQueryMockedStatic = mockStatic(DBQuery.class); + dbQueryMockedStatic + .when( + () -> + getAttachmentForID( + any(CdsEntity.class), any(PersistenceService.class), anyString())) + .thenReturn("file123.txt"); // Mock a different file name in SDM to trigger renaming + + when(sdmService.renameAttachments( + anyString(), any(SDMCredentials.class), any(CmisDocument.class))) + .thenReturn(403); // Mock conflict response code + + ServiceException exception = + assertThrows( + ServiceException.class, + () -> { + handler.updateName(context, data); + }); + + assertEquals(SDMConstants.SDM_MISSING_ROLES_EXCEPTION_MSG, exception.getMessage()); + } + + @Test + public void testRenameWith500Error() throws IOException { + // Mock the data structure to simulate the attachments + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = spy(new HashMap<>()); + attachment.put("fileName", "file1.txt"); + attachment.put("url", "objectId"); + attachment.put("ID", "test-id"); // assuming there's an ID field + attachments.add(attachment); + entity.put("attachments", attachments); + CdsData mockCdsData = mock(CdsData.class); + when(mockCdsData.get("attachments")).thenReturn(attachments); + data.add(mockCdsData); + + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(model.findEntity("some.qualified.Name.attachments")) + .thenReturn(Optional.of(attachmentDraftEntity)); + + // Mock the authentication context + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + + // Mock the static TokenHandler + when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + + // Mock the SDM service responses + dbQueryMockedStatic = mockStatic(DBQuery.class); + dbQueryMockedStatic + .when( + () -> + getAttachmentForID( + any(CdsEntity.class), any(PersistenceService.class), anyString())) + .thenReturn("file123.txt"); // Mock a different file name in SDM to trigger renaming + + when(sdmService.renameAttachments( + anyString(), any(SDMCredentials.class), any(CmisDocument.class))) + .thenReturn(500); // Mock conflict response code + + ServiceException exception = + assertThrows( + ServiceException.class, + () -> { + handler.updateName(context, data); + }); + + assertEquals(SDMConstants.SDM_ROLES_ERROR_MESSAGE, exception.getMessage()); + } + + @Test + public void testRenameWith200ResponseCode() throws IOException { + // Mock the data structure to simulate the attachments + System.out.println("testRenameWithConflictResponseCode"); + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = spy(new HashMap<>()); + attachment.put("fileName", "file1.txt"); + attachment.put("url", "objectId"); + attachment.put("ID", "test-id"); // assuming there's an ID field + attachments.add(attachment); + entity.put("attachments", attachments); + CdsData mockCdsData = mock(CdsData.class); + when(mockCdsData.get("attachments")).thenReturn(attachments); + data.add(mockCdsData); + + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(model.findEntity("some.qualified.Name.attachments")) + .thenReturn(Optional.of(attachmentDraftEntity)); + + // Mock the authentication context + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + + // Mock the static TokenHandler + when(TokenHandler.getSDMCredentials()).thenReturn(mockCredentials); + + // Mock the SDM service responses + dbQueryMockedStatic = mockStatic(DBQuery.class); + dbQueryMockedStatic + .when( + () -> + getAttachmentForID( + any(CdsEntity.class), any(PersistenceService.class), anyString())) + .thenReturn("file123.txt"); // Mock a different file name in SDM to trigger renaming + + when(sdmService.renameAttachments( + anyString(), any(SDMCredentials.class), any(CmisDocument.class))) + .thenReturn(200); // Mock conflict response code + + // Execute the method under test + handler.updateName(context, data); + + verify(attachment, never()).replace("fileName", "file-sdm.txt"); + + // Verify that a warning message was added to the context + verify(messages, times(0)) + .warn("The following files could not be renamed as they already exist:\nfile1.txt\n"); + } + + @Test + public void testRenameWithoutFileInSDM() throws IOException { + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(model.findEntity("some.qualified.Name.attachments")) + .thenReturn(Optional.of(attachmentDraftEntity)); + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + + List data = prepareMockAttachmentData("file1.txt"); + + dbQueryMockedStatic = mockStatic(DBQuery.class); + + dbQueryMockedStatic + .when( + () -> + getAttachmentForID( + any(CdsEntity.class), any(PersistenceService.class), anyString())) + .thenReturn(null); + + handler.updateName(context, data); + verify(sdmService, never()) + .renameAttachments(anyString(), any(SDMCredentials.class), any(CmisDocument.class)); + } + + @Test + public void testRenameWithNoAttachments() throws IOException { + List data = new ArrayList<>(); + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(model.findEntity("some.qualified.Name.attachments")) + .thenReturn(Optional.of(attachmentDraftEntity)); + CdsData mockCdsData = mock(CdsData.class); + when(mockCdsData.get("attachments")).thenReturn(null); + data.add(mockCdsData); + + handler.updateName(context, data); + + verify(sdmService, never()) + .renameAttachments(anyString(), any(SDMCredentials.class), any(CmisDocument.class)); + } + + @Test + public void testRenameWithRestrictedFilenames() throws IOException { + List data = prepareMockAttachmentData("file1.txt", "file2/abc.txt", "file3\\abc.txt"); + List fileNameWithRestrictedChars = new ArrayList<>(); + fileNameWithRestrictedChars.add("file2/abc.txt"); + fileNameWithRestrictedChars.add("file3\\abc.txt"); + + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(model.findEntity("some.qualified.Name.attachments")) + .thenReturn(Optional.of(attachmentDraftEntity)); + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + + when(context.getMessages()).thenReturn(messages); + + sdmUtilsMockedStatic = mockStatic(SDMUtils.class); + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) + .thenAnswer( + invocation -> { + String filename = invocation.getArgument(0); + return filename.contains("/") || filename.contains("\\"); + }); + + when(sdmService.renameAttachments( + anyString(), any(SDMCredentials.class), any(CmisDocument.class))) + .thenReturn(409); // Mock conflict response code + + dbQueryMockedStatic = mockStatic(DBQuery.class); + dbQueryMockedStatic + .when( + () -> + getAttachmentForID( + any(CdsEntity.class), any(PersistenceService.class), anyString())) + .thenReturn("file-in-sdm.txt"); + + handler.updateName(context, data); + + verify(messages, times(1)) + .warn(SDMConstants.nameConstraintMessage(fileNameWithRestrictedChars, "Rename")); + + verify(messages, never()).error(anyString()); + } + + @Test + public void testRenameWithValidRestrictedNames() throws IOException { + List data = new ArrayList<>(); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment = spy(new HashMap<>()); + List fileNameWithRestrictedChars = new ArrayList<>(); + fileNameWithRestrictedChars.add("file2/abc.txt"); + attachment.put("fileName", "file2/abc.txt"); + attachment.put("objectId", "objectId-123"); + attachment.put("ID", "id-123"); + attachments.add(attachment); + entity.put("attachments", attachments); + CdsData mockCdsData = mock(CdsData.class); + when(mockCdsData.get("attachments")).thenReturn(attachments); + data.add(mockCdsData); + + CdsEntity attachmentDraftEntity = mock(CdsEntity.class); + when(context.getTarget()).thenReturn(attachmentDraftEntity); + when(context.getModel()).thenReturn(model); + when(attachmentDraftEntity.getQualifiedName()).thenReturn("some.qualified.Name"); + when(model.findEntity("some.qualified.Name.attachments")) + .thenReturn(Optional.of(attachmentDraftEntity)); + when(context.getAuthenticationInfo()).thenReturn(authInfo); + when(authInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(jwtTokenInfo); + when(jwtTokenInfo.getToken()).thenReturn("jwtToken"); + + when(context.getMessages()).thenReturn(messages); + + sdmUtilsMockedStatic = mockStatic(SDMUtils.class); + sdmUtilsMockedStatic + .when(() -> SDMUtils.isRestrictedCharactersInName(anyString())) + .thenAnswer( + invocation -> { + String filename = invocation.getArgument(0); + return filename.contains("/") || filename.contains("\\"); + }); + + dbQueryMockedStatic = mockStatic(DBQuery.class); + dbQueryMockedStatic + .when( + () -> + getAttachmentForID( + any(CdsEntity.class), any(PersistenceService.class), anyString())) + .thenReturn("file3/abc.txt"); + + handler.updateName(context, data); + + // Verify the attachment's file name was replaced with the name in SDM + verify(attachment).replace("fileName", "file3/abc.txt"); + + // Verify that a warning message is correct + verify(messages, times(1)) + .warn( + String.format( + SDMConstants.nameConstraintMessage(fileNameWithRestrictedChars, "Rename"))); + } + + private List prepareMockAttachmentData(String... fileNames) { + List data = new ArrayList<>(); + for (String fileName : fileNames) { + CdsData cdsData = mock(CdsData.class); + List> attachments = new ArrayList<>(); + Map attachment = new HashMap<>(); + attachment.put("ID", UUID.randomUUID().toString()); + attachment.put("fileName", fileName); + attachment.put("url", "objectId"); + attachments.add(attachment); + when(cdsData.get("attachments")).thenReturn(attachments); + data.add(cdsData); + } + return data; + } +} diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/service/SDMServiceImplTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/service/SDMServiceImplTest.java new file mode 100644 index 00000000..39e57b26 --- /dev/null +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/service/SDMServiceImplTest.java @@ -0,0 +1,1229 @@ +package unit.com.sap.cds.sdm.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import com.google.gson.JsonObject; +import com.sap.cds.Result; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; +import com.sap.cds.sdm.caching.CacheConfig; +import com.sap.cds.sdm.caching.RepoKey; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.TokenHandler; +import com.sap.cds.sdm.model.CmisDocument; +import com.sap.cds.sdm.model.SDMCredentials; +import com.sap.cds.sdm.service.*; +import com.sap.cds.services.ServiceException; +import com.sap.cds.services.environment.CdsProperties; +import com.sap.cds.services.persistence.PersistenceService; +import com.sap.cloud.environment.servicebinding.api.ServiceBinding; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.*; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.apache.http.HttpEntity; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.ehcache.Cache; +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +public class SDMServiceImplTest { + private static final String REPO_ID = "repo"; + private SDMService SDMService; + JsonObject expected; + RepoKey repoKey; + @Mock ServiceBinding binding; + @Mock CdsProperties.ConnectionPool connectionPool; + String subdomain = "SUBDOMAIN"; + + private CloseableHttpClient httpClient; + private CloseableHttpResponse response; + + StatusLine statusLine; + HttpEntity entity; + + @BeforeEach + public void setUp() { + httpClient = mock(CloseableHttpClient.class); + response = mock(CloseableHttpResponse.class); + statusLine = mock(StatusLine.class); + entity = mock(HttpEntity.class); + SDMService = new SDMServiceImpl(binding, connectionPool); + repoKey = new RepoKey(); + expected = new JsonObject(); + expected.addProperty( + "email", "john.doe@example.com"); // Correct the property name as expected in the method + expected.addProperty( + "exp", "1234567890"); // Correct the property name as expected in the method + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("zdn", "tenant"); + expected.add("ext_attr", jsonObject); + repoKey.setRepoId("repo"); + repoKey.setSubdomain("tenant"); + } + + @Test + public void testIsRepositoryVersioned_Versioned() throws IOException { + // Mocked JSON structure for a versioned repository + JSONObject capabilities = new JSONObject(); + capabilities.put("capabilityContentStreamUpdatability", "pwconly"); + + JSONObject repoInfo = new JSONObject(); + repoInfo.put("capabilities", capabilities); + + JSONObject root = new JSONObject(); + root.put(REPO_ID, repoInfo); + + // Call the method and verify the result + boolean isVersioned = SDMService.isRepositoryVersioned(root, REPO_ID); + assertTrue(isVersioned); + } + + @Test + public void testIsRepositoryVersioned_NonVersioned() throws IOException { + // Mocked JSON structure for a non-versioned repository + JSONObject capabilities = new JSONObject(); + capabilities.put("capabilityContentStreamUpdatability", "other"); + + JSONObject repoInfo = new JSONObject(); + repoInfo.put("capabilities", capabilities); + + JSONObject root = new JSONObject(); + root.put(REPO_ID, repoInfo); + + // Call the method and verify the result + boolean isVersioned = SDMService.isRepositoryVersioned(root, REPO_ID); + assertFalse(isVersioned); + } + + @Test + public void testGetRepositoryInfo() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class); ) { + JSONObject capabilities = new JSONObject(); + capabilities.put("capabilityContentStreamUpdatability", "other"); + JSONObject repoInfo = new JSONObject(); + repoInfo.put("capabilities", capabilities); + JSONObject root = new JSONObject(); + root.put(REPO_ID, repoInfo); + tokenHandlerMockedStatic + .when( + () -> + TokenHandler.getHttpClient(any(), any(), any(), eq("TECHNICAL_CREDENTIALS_FLOW"))) + .thenReturn(httpClient); + when(httpClient.execute(any(HttpGet.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(200); + when((response.getEntity())).thenReturn(entity); + InputStream inputStream = new ByteArrayInputStream(root.toString().getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + SDMCredentials sdmCredentials = new SDMCredentials(); + sdmCredentials.setUrl("test"); + com.sap.cds.sdm.service.SDMService sdmService = new SDMServiceImpl(binding, connectionPool); + JSONObject json = sdmService.getRepositoryInfo(sdmCredentials, subdomain); + + JSONObject fetchedRepoInfo = json.getJSONObject(REPO_ID); + JSONObject fetchedCapabilities = fetchedRepoInfo.getJSONObject("capabilities"); + assertEquals("other", fetchedCapabilities.getString("capabilityContentStreamUpdatability")); + } + } + + @Test + public void testGetRepositoryInfoFail() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class); ) { + SDMCredentials sdmCredentials = new SDMCredentials(); + sdmCredentials.setUrl("test"); + String token = "token"; + com.sap.cds.sdm.service.SDMService sdmService = new SDMServiceImpl(binding, connectionPool); + SDMCredentials mockSdmCredentials = new SDMCredentials(); + mockSdmCredentials.setUrl("test"); + tokenHandlerMockedStatic + .when( + () -> + TokenHandler.getHttpClient(any(), any(), any(), eq("TECHNICAL_CREDENTIALS_FLOW"))) + .thenReturn(httpClient); + when(httpClient.execute(any(HttpGet.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(500); + + ServiceException exception = + assertThrows( + ServiceException.class, + () -> { + sdmService.getRepositoryInfo(sdmCredentials, subdomain); + }); + assertEquals("Failed to get repository info.", exception.getMessage()); + } + } + + @Test + public void testCheckRepositoryTypeCacheVersioned() throws IOException { + String repositoryId = "repo"; + String token = "token"; + try (MockedStatic cacheConfigMockedStatic = Mockito.mockStatic(CacheConfig.class); + MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class); ) { + Cache mockCache = Mockito.mock(Cache.class); + tokenHandlerMockedStatic.when(() -> TokenHandler.getTokenFields(token)).thenReturn(expected); + when(httpClient.execute(any(HttpGet.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(500); + Mockito.when(mockCache.get(repoKey)).thenReturn("Versioned"); + cacheConfigMockedStatic.when(CacheConfig::getVersionedRepoCache).thenReturn(mockCache); + String result = SDMService.checkRepositoryType(token, repositoryId); + assertEquals("Versioned", result); + } + } + + @Test + public void testCheckRepositoryTypeCacheNonVersioned() throws IOException { + String repositoryId = "repo"; + String token = "token"; + try (MockedStatic cacheConfigMockedStatic = Mockito.mockStatic(CacheConfig.class); + MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class); ) { + Cache mockCache = Mockito.mock(Cache.class); + SDMCredentials mockSdmCredentials = new SDMCredentials(); + mockSdmCredentials.setUrl("test"); + tokenHandlerMockedStatic + .when( + () -> + TokenHandler.getHttpClient(any(), any(), any(), eq("TECHNICAL_CREDENTIALS_FLOW"))) + .thenReturn(httpClient); + tokenHandlerMockedStatic.when(() -> TokenHandler.getTokenFields(token)).thenReturn(expected); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getSDMCredentials()) + .thenReturn(mockSdmCredentials); + when(httpClient.execute(any(HttpGet.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(200); + when((response.getEntity())).thenReturn(entity); + Mockito.when(mockCache.get(repoKey)).thenReturn("Non Versioned"); + cacheConfigMockedStatic.when(CacheConfig::getVersionedRepoCache).thenReturn(mockCache); + String result = SDMService.checkRepositoryType(token, repositoryId); + assertEquals("Non Versioned", result); + } + } + + @Test + public void testCheckRepositoryTypeNoCacheVersioned() throws IOException { + String repositoryId = "repo"; + String token = "token"; + SDMServiceImpl spySDMService = Mockito.spy(new SDMServiceImpl(binding, connectionPool)); + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class); + MockedStatic cacheConfigMockedStatic = Mockito.mockStatic(CacheConfig.class)) { + Cache mockCache = Mockito.mock(Cache.class); + tokenHandlerMockedStatic.when(() -> TokenHandler.getTokenFields(token)).thenReturn(expected); + Mockito.when(mockCache.get(repoKey)).thenReturn(null); + cacheConfigMockedStatic.when(CacheConfig::getVersionedRepoCache).thenReturn(mockCache); + SDMCredentials mockSdmCredentials = new SDMCredentials(); + mockSdmCredentials.setUrl("test"); + tokenHandlerMockedStatic + .when( + () -> + TokenHandler.getHttpClient(any(), any(), any(), eq("TECHNICAL_CREDENTIALS_FLOW"))) + .thenReturn(httpClient); + HttpGet getRepoInfoRequest = + new HttpGet( + mockSdmCredentials.getUrl() + + "browser/" + + repositoryId + + "?cmisselector=repositoryInfo"); + tokenHandlerMockedStatic.when(() -> TokenHandler.getTokenFields(token)).thenReturn(expected); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getSDMCredentials()) + .thenReturn(mockSdmCredentials); + when(httpClient.execute(any(HttpGet.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(200); + when((response.getEntity())).thenReturn(entity); + JSONObject capabilities = new JSONObject(); + capabilities.put( + "capabilityContentStreamUpdatability", + "pwconly"); // To match the expected output "Versioned" + JSONObject repoInfo = new JSONObject(); + repoInfo.put("capabilities", capabilities); + JSONObject mockRepoData = new JSONObject(); + mockRepoData.put(repositoryId, repoInfo); + InputStream inputStream = new ByteArrayInputStream(mockRepoData.toString().getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + String result = spySDMService.checkRepositoryType(token, repositoryId); + assertEquals("Versioned", result); + } + } + + @Test + public void testCheckRepositoryTypeNoCacheNonVersioned() throws IOException { + String repositoryId = "repo"; + String token = "token"; + SDMServiceImpl spySDMService = Mockito.spy(new SDMServiceImpl(binding, connectionPool)); + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class); + MockedStatic cacheConfigMockedStatic = Mockito.mockStatic(CacheConfig.class)) { + + Cache mockCache = Mockito.mock(Cache.class); + Mockito.when(mockCache.get(repoKey)).thenReturn(null); + cacheConfigMockedStatic.when(CacheConfig::getVersionedRepoCache).thenReturn(mockCache); + SDMCredentials mockSdmCredentials = new SDMCredentials(); + mockSdmCredentials.setUrl("test"); + tokenHandlerMockedStatic + .when( + () -> + TokenHandler.getHttpClient(any(), any(), any(), eq("TECHNICAL_CREDENTIALS_FLOW"))) + .thenReturn(httpClient); + HttpGet getRepoInfoRequest = + new HttpGet( + mockSdmCredentials.getUrl() + + "browser/" + + repositoryId + + "?cmisselector=repositoryInfo"); + tokenHandlerMockedStatic.when(() -> TokenHandler.getTokenFields(token)).thenReturn(expected); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getSDMCredentials()) + .thenReturn(mockSdmCredentials); + when(httpClient.execute(any(HttpGet.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(200); + + JSONObject capabilities = new JSONObject(); + capabilities.put( + "capabilityContentStreamUpdatability", + "notpwconly"); // To match the expected output "Versioned" + JSONObject repoInfo = new JSONObject(); + repoInfo.put("capabilities", capabilities); + JSONObject mockRepoData = new JSONObject(); + mockRepoData.put(repositoryId, repoInfo); + + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = new ByteArrayInputStream(mockRepoData.toString().getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + String result = spySDMService.checkRepositoryType(token, repositoryId); + assertEquals("Non Versioned", result); + } + } + + @Test + public void testCreateFolder() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + String expectedResponse = "Folder ID"; + + String parentId = "123"; + String jwtToken = "jwt_token"; + String repositoryId = "repository_id"; + SDMCredentials sdmCredentials = new SDMCredentials(); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(201); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = new ByteArrayInputStream(expectedResponse.getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + String actualResponse = + sdmServiceImpl.createFolder(parentId, repositoryId, sdmCredentials, jwtToken); + + assertEquals(expectedResponse, actualResponse); + } + } + + @Test + public void testCreateFolderFail() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + String parentId = "123"; + String jwtToken = "jwt_token"; + String repositoryId = "repository_id"; + SDMCredentials sdmCredentials = new SDMCredentials(); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(500); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = + new ByteArrayInputStream( + "Failed to create folder. Could not upload the document".getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + ServiceException exception = + assertThrows( + ServiceException.class, + () -> { + sdmServiceImpl.createFolder(parentId, repositoryId, sdmCredentials, jwtToken); + }); + assertEquals( + "Failed to create folder. Failed to create folder. Could not upload the document", + exception.getMessage()); + } + } + + @Test + public void testCreateFolderFailResponseCode403() throws IOException { + MockWebServer mockWebServer = new MockWebServer(); + mockWebServer.start(); + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + mockWebServer.enqueue( + new MockResponse() + .setResponseCode(403) // Set HTTP status code to 403 + .setBody("{\"error\":" + SDMConstants.USER_NOT_AUTHORISED_ERROR + "\"}") + .addHeader("Content-Type", "application/json")); + String parentId = "123"; + String jwtToken = "jwt_token"; + String repositoryId = "repository_id"; + String mockUrl = mockWebServer.url("/").toString(); + SDMCredentials sdmCredentials = new SDMCredentials(); + sdmCredentials.setUrl(mockUrl); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(403); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = + new ByteArrayInputStream(SDMConstants.USER_NOT_AUTHORISED_ERROR.getBytes()); + when(entity.getContent()).thenReturn(inputStream); + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + ServiceException exception = + assertThrows( + ServiceException.class, + () -> { + sdmServiceImpl.createFolder(parentId, repositoryId, sdmCredentials, jwtToken); + }); + assertEquals(SDMConstants.USER_NOT_AUTHORISED_ERROR, exception.getMessage()); + + } finally { + mockWebServer.shutdown(); + } + } + + @Test + public void testGetFolderIdByPath() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + String expectedResponse = + "{" + + "\"properties\": {" + + "\"cmis:objectId\": {" + + "\"id\": \"cmis:objectId\"," + + "\"localName\": \"cmis:objectId\"," + + "\"displayName\": \"cmis:objectId\"," + + "\"queryName\": \"cmis:objectId\"," + + "\"type\": \"id\"," + + "\"cardinality\": \"single\"," + + "\"value\": \"ExpectedFolderId\"" + + "}}" + + "}"; + + String parentId = "123"; + String jwtToken = "jwt_token"; + String repositoryId = "repository_id"; + SDMCredentials sdmCredentials = new SDMCredentials(); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(200); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = new ByteArrayInputStream("ExpectedFolderId".getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + String actualResponse = + sdmServiceImpl.getFolderIdByPath(parentId, repositoryId, sdmCredentials, jwtToken); + + assertEquals("ExpectedFolderId", actualResponse); + } + } + + @Test + public void testGetFolderIdByPathFail() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + String parentId = "123"; + String jwtToken = "jwt_token"; + String repositoryId = "repository_id"; + SDMCredentials sdmCredentials = new SDMCredentials(); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(500); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = new ByteArrayInputStream("Internal Server".getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + String folderId = + sdmServiceImpl.getFolderIdByPath(parentId, repositoryId, sdmCredentials, jwtToken); + assertNull(folderId, "Expected folderId to be null"); + } + } + + @Test + public void testGetFolderIdByPathFailResponseCode403() throws IOException { + MockWebServer mockWebServer = new MockWebServer(); + mockWebServer.start(); + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + mockWebServer.enqueue( + new MockResponse() + .setResponseCode(403) // Set HTTP status code to 403 for an internal server error + .setBody("{\"error\":" + SDMConstants.USER_NOT_AUTHORISED_ERROR + "\"}") + // the body + .addHeader("Content-Type", "application/json")); + String parentId = "123"; + String jwtToken = "jwt_token"; + String repositoryId = "repository_id"; + String mockUrl = mockWebServer.url("/").toString(); + SDMCredentials sdmCredentials = new SDMCredentials(); + sdmCredentials.setUrl(mockUrl); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(403); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = + new ByteArrayInputStream( + "Failed to create folder. Could not upload the document".getBytes()); + when(entity.getContent()).thenReturn(inputStream); + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + ServiceException exception = + assertThrows( + ServiceException.class, + () -> { + sdmServiceImpl.getFolderIdByPath(parentId, repositoryId, sdmCredentials, jwtToken); + }); + assertEquals(SDMConstants.USER_NOT_AUTHORISED_ERROR, exception.getMessage()); + + } finally { + mockWebServer.shutdown(); + } + } + + @Test + public void testCreateDocument() throws IOException { + + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + String mockResponseBody = "{\"succinctProperties\": {\"cmis:objectId\": \"objectId\"}}"; + + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setFileName("sample.pdf"); + cmisDocument.setAttachmentId("attachmentId"); + String content = "sample.pdf content"; + InputStream contentStream = + new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + cmisDocument.setContent(contentStream); + cmisDocument.setParentId("parentId"); + cmisDocument.setRepositoryId("repositoryId"); + cmisDocument.setFolderId("folderId"); + cmisDocument.setMimeType("application/pdf"); + + String jwtToken = "jwtToken"; + SDMCredentials sdmCredentials = new SDMCredentials(); + + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(201); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = new ByteArrayInputStream(mockResponseBody.getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + JSONObject actualResponse = + sdmServiceImpl.createDocument(cmisDocument, sdmCredentials, jwtToken); + + JSONObject expectedResponse = new JSONObject(); + expectedResponse.put("name", "sample.pdf"); + expectedResponse.put("id", "attachmentId"); + expectedResponse.put("objectId", "objectId"); + expectedResponse.put("message", ""); + expectedResponse.put("status", "success"); + assertEquals(expectedResponse.toString(), actualResponse.toString()); + } + } + + @Test + public void testCreateDocumentFailDuplicate() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + String mockResponseBody = "{\"message\": \"Duplicate document found\"}"; + + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setFileName("sample.pdf"); + cmisDocument.setAttachmentId("attachmentId"); + String content = "sample.pdf content"; + InputStream contentStream = + new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + cmisDocument.setContent(contentStream); + cmisDocument.setParentId("parentId"); + cmisDocument.setRepositoryId("repositoryId"); + cmisDocument.setFolderId("folderId"); + cmisDocument.setMimeType("application/pdf"); + + String jwtToken = "jwtToken"; + SDMCredentials sdmCredentials = new SDMCredentials(); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(409); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = new ByteArrayInputStream(mockResponseBody.getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + JSONObject actualResponse = + sdmServiceImpl.createDocument(cmisDocument, sdmCredentials, jwtToken); + + JSONObject expectedResponse = new JSONObject(); + expectedResponse.put("name", "sample.pdf"); + expectedResponse.put("id", "attachmentId"); + expectedResponse.put("message", ""); + expectedResponse.put("status", "duplicate"); + assertEquals(expectedResponse.toString(), actualResponse.toString()); + } + } + + @Test + public void testCreateDocumentFailVirus() throws IOException { + + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + String mockResponseBody = + "{\"message\": \"Malware Service Exception: Virus found in the file!\"}"; + + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setFileName("sample.pdf"); + cmisDocument.setAttachmentId("attachmentId"); + String content = "sample.pdf content"; + InputStream contentStream = + new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + cmisDocument.setContent(contentStream); + cmisDocument.setParentId("parentId"); + cmisDocument.setRepositoryId("repositoryId"); + cmisDocument.setFolderId("folderId"); + cmisDocument.setMimeType("application/pdf"); + + String jwtToken = "jwtToken"; + SDMCredentials sdmCredentials = new SDMCredentials(); + + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(409); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = new ByteArrayInputStream(mockResponseBody.getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + JSONObject actualResponse = + sdmServiceImpl.createDocument(cmisDocument, sdmCredentials, jwtToken); + + JSONObject expectedResponse = new JSONObject(); + expectedResponse.put("name", "sample.pdf"); + expectedResponse.put("id", "attachmentId"); + expectedResponse.put("message", ""); + expectedResponse.put("status", "virus"); + assertEquals(expectedResponse.toString(), actualResponse.toString()); + } + } + + @Test + public void testCreateDocumentFailOther() throws IOException { + + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + String mockResponseBody = "{\"message\": \"An unexpected error occurred\"}"; + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setFileName("sample.pdf"); + cmisDocument.setAttachmentId("attachmentId"); + String content = "sample.pdf content"; + InputStream contentStream = + new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + cmisDocument.setContent(contentStream); + cmisDocument.setParentId("parentId"); + cmisDocument.setRepositoryId("repositoryId"); + cmisDocument.setFolderId("folderId"); + cmisDocument.setMimeType("application/pdf"); + + String jwtToken = "jwtToken"; + SDMCredentials sdmCredentials = new SDMCredentials(); + + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(500); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = new ByteArrayInputStream(mockResponseBody.getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + JSONObject actualResponse = + sdmServiceImpl.createDocument(cmisDocument, sdmCredentials, jwtToken); + + JSONObject expectedResponse = new JSONObject(); + expectedResponse.put("name", "sample.pdf"); + expectedResponse.put("id", "attachmentId"); + expectedResponse.put("message", "An unexpected error occurred"); + expectedResponse.put("status", "fail"); + assertEquals(expectedResponse.toString(), actualResponse.toString()); + } + } + + @Test + public void testCreateDocumentFailRequestError() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setFileName("sample.pdf"); + cmisDocument.setAttachmentId("attachmentId"); + String content = "sample.pdf content"; + InputStream contentStream = + new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + cmisDocument.setContent(contentStream); + cmisDocument.setParentId("parentId"); + cmisDocument.setRepositoryId("repositoryId"); + cmisDocument.setFolderId("folderId"); + cmisDocument.setMimeType("application/pdf"); + String jwtToken = "jwtToken"; + SDMCredentials sdmCredentials = new SDMCredentials(); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(500); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = + new ByteArrayInputStream("{\"message\":\"Error in setting timeout\"}".getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + try { + sdmServiceImpl.createDocument(cmisDocument, sdmCredentials, jwtToken); + } catch (ServiceException e) { + // Expected exception to be thrown + assertEquals("Error in setting timeout", e.getMessage()); + } + } + } + + @Test + public void testDeleteFolder() throws IOException { + MockWebServer mockWebServer = new MockWebServer(); + mockWebServer.start(); + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + String expectedResponse = "200"; + mockWebServer.enqueue( + new MockResponse().setResponseCode(200).addHeader("Content-Type", "application/json")); + String mockUrl = mockWebServer.url("/").toString(); + SDMCredentials sdmCredentials = new SDMCredentials(); + sdmCredentials.setUrl(mockUrl); + Mockito.when(TokenHandler.getSDMCredentials()).thenReturn(sdmCredentials); + + Mockito.when(TokenHandler.getDITokenUsingAuthorities(sdmCredentials, "email", "subdomain")) + .thenReturn("mockAccessToken"); + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + int actualResponse = + sdmServiceImpl.deleteDocument("deleteTree", "objectId", "email", "subdomain"); + + assertEquals(200, actualResponse); + + } finally { + mockWebServer.shutdown(); + } + } + + @Test + void testGetFolderId_FolderIdPresentInResult() throws IOException { + PersistenceService persistenceService = mock(PersistenceService.class); + Result result = mock(Result.class); + Map attachment = new HashMap<>(); + attachment.put("folderId", "newFolderId123"); + attachment.put("repositoryId", "repoId"); + List resultList = Arrays.asList((Map) attachment); + + when(result.listOf(Map.class)).thenReturn((List) resultList); + + String jwtToken = "jwtToken"; + String up__ID = "up__ID"; + + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + SDMCredentials sdmCredentials = new SDMCredentials(); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + tokenHandlerMockedStatic.when(TokenHandler::getSDMCredentials).thenReturn(sdmCredentials); + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(500); + when(response.getEntity()).thenReturn(entity); + + // Mock the method `getFolderIdByPath` + SDMServiceImpl spyService = spy(sdmServiceImpl); + doReturn(null) + .when(spyService) + .getFolderIdByPath(anyString(), anyString(), any(SDMCredentials.class), anyString()); + + // Mock the method `createFolder` + doReturn("{\"succinctProperties\":{\"cmis:objectId\":\"newFolderId123\"}}") + .when(spyService) + .createFolder(anyString(), anyString(), any(SDMCredentials.class), anyString()); + + String folderId = spyService.getFolderId(result, persistenceService, up__ID, jwtToken); + assertEquals("newFolderId123", folderId, "Expected folderId from result list"); + } + } + + @Test + public void testDeleteDocument() throws IOException { + MockWebServer mockWebServer = new MockWebServer(); + mockWebServer.start(); + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + String expectedResponse = "200"; + mockWebServer.enqueue( + new MockResponse().setResponseCode(200).addHeader("Content-Type", "application/json")); + String mockUrl = mockWebServer.url("/").toString(); + SDMCredentials sdmCredentials = new SDMCredentials(); + sdmCredentials.setUrl(mockUrl); + Mockito.when(TokenHandler.getSDMCredentials()).thenReturn(sdmCredentials); + + Mockito.when(TokenHandler.getDITokenUsingAuthorities(sdmCredentials, "email", "subdomain")) + .thenReturn("mockAccessToken"); + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + int actualResponse = + sdmServiceImpl.deleteDocument("delete", "objectId", "email", "subdomain"); + + assertEquals(200, actualResponse); + + } finally { + mockWebServer.shutdown(); + } + } + + @Test + public void testDeleteDocumentObjectNotFound() throws IOException { + MockWebServer mockWebServer = new MockWebServer(); + mockWebServer.start(); + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + String mockResponseBody = "{\"message\": \"Object Not Found\"}"; + mockWebServer.enqueue( + new MockResponse() + .setBody(mockResponseBody) + .setResponseCode(404) // Assuming 400 Bad Request or a similar client error code + .addHeader("Content-Type", "application/json")); + String mockUrl = mockWebServer.url("/").toString(); + SDMCredentials sdmCredentials = new SDMCredentials(); + sdmCredentials.setUrl(mockUrl); + Mockito.when(TokenHandler.getSDMCredentials()).thenReturn(sdmCredentials); + + Mockito.when(TokenHandler.getDITokenUsingAuthorities(sdmCredentials, "email", "subdomain")) + .thenReturn("mockAccessToken"); + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + int actualResponse = sdmServiceImpl.deleteDocument("delete", "ewdwe", "email", "subdomain"); + + assertEquals(404, actualResponse); + + } finally { + mockWebServer.shutdown(); + } + } + + @Test + public void testGetDITokenUsingAuthoritiesThrowsIOException() { + String cmisaction = "someAction"; + String objectId = "someObjectId"; + String userEmail = "user@example.com"; + String subdomain = "testSubdomain"; + + SDMCredentials sdmCredentials = new SDMCredentials(); + sdmCredentials.setUrl("https://example.com/"); + + // Mocking static methods + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + tokenHandlerMockedStatic.when(TokenHandler::getSDMCredentials).thenReturn(sdmCredentials); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getDITokenUsingAuthorities(sdmCredentials, userEmail, subdomain)) + .thenThrow(new IOException("Could not delete the document.")); + + // Since the exception is thrown before OkHttpClient is used, no need to mock httpClient + // behavior. + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + // Assert exception + IOException thrown = + assertThrows( + IOException.class, + () -> { + // Call the method under test + sdmServiceImpl.deleteDocument(cmisaction, objectId, userEmail, subdomain); + }); + + // Verify the exception message + assertEquals("Could not delete the document.", thrown.getMessage()); + } + } + + @Test + void testGetFolderId_GetFolderIdByPathReturns() throws IOException { + Result result = mock(Result.class); + PersistenceService persistenceService = mock(PersistenceService.class); + + List resultList = new ArrayList<>(); + when(result.listOf(Map.class)).thenReturn((List) resultList); + + String jwtToken = "jwtToken"; + String up__ID = "up__ID"; + + SDMServiceImpl sdmServiceImpl = spy(new SDMServiceImpl(binding, connectionPool)); + + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + doReturn("folderByPath123") + .when(sdmServiceImpl) + .getFolderIdByPath(anyString(), anyString(), any(SDMCredentials.class), anyString()); + + SDMCredentials sdmCredentials = new SDMCredentials(); + sdmCredentials.setUrl("mockUrl"); + MockWebServer mockWebServer = new MockWebServer(); + mockWebServer.start(); + String mockUrl = mockWebServer.url("/").toString(); + sdmCredentials.setUrl(mockUrl); + tokenHandlerMockedStatic.when(TokenHandler::getSDMCredentials).thenReturn(sdmCredentials); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(500); + when(response.getEntity()).thenReturn(entity); + + MockResponse mockResponse1 = + new MockResponse().setResponseCode(200).setBody("folderByPath123"); + mockWebServer.enqueue(mockResponse1); + String folderId = sdmServiceImpl.getFolderId(result, persistenceService, up__ID, jwtToken); + assertEquals("folderByPath123", folderId, "Expected folderId from getFolderIdByPath"); + } + } + + @Test + void testGetFolderId_CreateFolderWhenFolderIdNull() throws IOException { + // Mock the dependencies + Result result = mock(Result.class); + PersistenceService persistenceService = mock(PersistenceService.class); + + // Mock the result list as empty + List resultList = new ArrayList<>(); + when(result.listOf(Map.class)).thenReturn((List) resultList); + + String jwtToken = "jwtToken"; + String up__ID = "up__ID"; + + // Create a spy of the SDMServiceImpl to mock specific methods + SDMServiceImpl sdmServiceImpl = spy(new SDMServiceImpl(binding, connectionPool)); + + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + // Mock the getFolderIdByPath method to return null (so that it will try to create a folder) + doReturn(null) + .when(sdmServiceImpl) + .getFolderIdByPath(anyString(), anyString(), any(SDMCredentials.class), anyString()); + + // Mock the TokenHandler static method and SDMCredentials instantiation + SDMCredentials sdmCredentials = new SDMCredentials(); + sdmCredentials.setUrl("mockUrl"); + + // Use MockWebServer to set the URL for SDMCredentials + MockWebServer mockWebServer = new MockWebServer(); + mockWebServer.start(); + String mockUrl = mockWebServer.url("/").toString(); + sdmCredentials.setUrl(mockUrl); + + // Mock the static method to return a valid SDMCredentials instance + tokenHandlerMockedStatic.when(TokenHandler::getSDMCredentials).thenReturn(sdmCredentials); + + // Mock the token retrieval as well + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(500); + when(response.getEntity()).thenReturn(entity); + + // Mock the createFolder method to return a folder ID when invoked + JSONObject jsonObject = new JSONObject(); + JSONObject succinctProperties = new JSONObject(); + succinctProperties.put("cmis:objectId", "newFolderId123"); + jsonObject.put("succinctProperties", succinctProperties); + + // Enqueue the mock response on the MockWebServer + MockResponse mockResponse1 = + new MockResponse().setResponseCode(200).setBody("newFolderId123"); + mockWebServer.enqueue(mockResponse1); + + doReturn(jsonObject.toString()) + .when(sdmServiceImpl) + .createFolder(anyString(), anyString(), any(SDMCredentials.class), anyString()); + + // Invoke the method + String folderId = sdmServiceImpl.getFolderId(result, persistenceService, up__ID, jwtToken); + + // Assert the folder ID is the newly created one + assertEquals("newFolderId123", folderId, "Expected newly created folderId"); + } + } + + @Test + public void testReadDocument_Success() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + String expectedContent = "This is a document content."; + String objectId = "testObjectId"; + String jwtToken = "testJwtToken"; + String repositoryId = "repository_id"; + SDMCredentials sdmCredentials = new SDMCredentials(); + AttachmentReadEventContext mockContext = mock(AttachmentReadEventContext.class); + MediaData mockData = mock(MediaData.class); + when(mockContext.getData()).thenReturn(mockData); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpGet.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(200); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = + new ByteArrayInputStream("{\"message\":\"Server error\"}".getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + sdmServiceImpl.readDocument(objectId, jwtToken, sdmCredentials, mockContext); + + verify(mockData).setContent(any(InputStream.class)); + } + } + + @Test + public void testReadDocument_UnsuccessfulResponse() throws IOException { + + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + String objectId = "testObjectId"; + String jwtToken = "testJwtToken"; + SDMCredentials sdmCredentials = new SDMCredentials(); + AttachmentReadEventContext mockContext = mock(AttachmentReadEventContext.class); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpGet.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(500); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = + new ByteArrayInputStream("{\"message\":\"Server error\"}".getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + ServiceException exception = + assertThrows( + ServiceException.class, + () -> { + sdmServiceImpl.readDocument(objectId, jwtToken, sdmCredentials, mockContext); + }); + + // Check if the exception message contains the expected first part + String expectedMessagePart1 = "Failed to set document stream in context"; + assertTrue(exception.getMessage().contains(expectedMessagePart1)); + } + } + + @Test + public void testReadDocument_ExceptionWhileSettingContent() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + String expectedContent = "This is a document content."; + String objectId = "testObjectId"; + String jwtToken = "testJwtToken"; + SDMCredentials sdmCredentials = new SDMCredentials(); + AttachmentReadEventContext mockContext = mock(AttachmentReadEventContext.class); + MediaData mockData = mock(MediaData.class); + when(mockContext.getData()).thenReturn(mockData); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpGet.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(500); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = new ByteArrayInputStream(expectedContent.getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + doThrow(new RuntimeException("Failed to set document stream in context")) + .when(mockData) + .setContent(any(InputStream.class)); + + ServiceException exception = + assertThrows( + ServiceException.class, + () -> { + sdmServiceImpl.readDocument(objectId, jwtToken, sdmCredentials, mockContext); + }); + assertEquals("Failed to set document stream in context", exception.getMessage()); + } + } + + @Test + public void testRenameAttachments_Success() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = mockStatic(TokenHandler.class)) { + String jwtToken = "jwt_token"; + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setFileName("newFileName"); + cmisDocument.setObjectId("objectId"); + + SDMCredentials mockSdmCredentials = mock(SDMCredentials.class); + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpPost.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(200); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = new ByteArrayInputStream("".getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + SDMServiceImpl sdmServiceImpl = new SDMServiceImpl(binding, connectionPool); + + int responseCode = + sdmServiceImpl.renameAttachments(jwtToken, mockSdmCredentials, cmisDocument); + + // Verify the response code + assertEquals(200, responseCode); + } + } + + @Test + public void testGetObject_Success() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + String mockResponseBody = "{\"succinctProperties\": {\"cmis:name\":\"desiredObjectName\"}}"; + String jwtToken = "jwt_token"; + String objectId = "objectId"; + SDMServiceImpl sdmServiceImpl = Mockito.spy(new SDMServiceImpl(binding, connectionPool)); + SDMCredentials sdmCredentials = new SDMCredentials(); + + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpGet.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(200); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = new ByteArrayInputStream(mockResponseBody.getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + String objectName = sdmServiceImpl.getObject(jwtToken, objectId, sdmCredentials); + assertEquals("desiredObjectName", objectName); + } + } + + @Test + public void testGetObject_Failure() throws IOException { + try (MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + String jwtToken = "jwt_token"; + String objectId = "objectId"; + SDMServiceImpl sdmServiceImpl = Mockito.spy(new SDMServiceImpl(binding, connectionPool)); + SDMCredentials sdmCredentials = new SDMCredentials(); + + tokenHandlerMockedStatic + .when(() -> TokenHandler.getHttpClient(any(), any(), any(), eq("TOKEN_EXCHANGE"))) + .thenReturn(httpClient); + + when(httpClient.execute(any(HttpGet.class))).thenReturn(response); + when(response.getStatusLine()).thenReturn(statusLine); + when(statusLine.getStatusCode()).thenReturn(500); + when(response.getEntity()).thenReturn(entity); + InputStream inputStream = new ByteArrayInputStream("".getBytes()); + when(entity.getContent()).thenReturn(inputStream); + + String objectName = sdmServiceImpl.getObject(jwtToken, objectId, sdmCredentials); + assertNull(objectName); + } + } +} 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 new file mode 100644 index 00000000..ee9abda3 --- /dev/null +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/service/handler/SDMAttachmentsServiceHandlerTest.java @@ -0,0 +1,769 @@ +package unit.com.sap.cds.sdm.service.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import com.google.gson.JsonObject; +import com.sap.cds.CdsData; +import com.sap.cds.Result; +import com.sap.cds.Row; +import com.sap.cds.feature.attachments.generated.cds4j.sap.attachments.MediaData; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentCreateEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentMarkAsDeletedEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentReadEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.AttachmentRestoreEventContext; +import com.sap.cds.feature.attachments.service.model.servicehandler.DeletionUserInfo; +import com.sap.cds.reflect.CdsEntity; +import com.sap.cds.reflect.CdsModel; +import com.sap.cds.sdm.constants.SDMConstants; +import com.sap.cds.sdm.handler.TokenHandler; +import com.sap.cds.sdm.model.CmisDocument; +import com.sap.cds.sdm.model.SDMCredentials; +import com.sap.cds.sdm.persistence.DBQuery; +import com.sap.cds.sdm.service.SDMService; +import com.sap.cds.sdm.service.SDMServiceImpl; +import com.sap.cds.sdm.service.handler.SDMAttachmentsServiceHandler; +import com.sap.cds.services.ServiceException; +import com.sap.cds.services.authentication.AuthenticationInfo; +import com.sap.cds.services.authentication.JwtTokenAuthenticationInfo; +import com.sap.cds.services.messages.Message; +import com.sap.cds.services.messages.Messages; +import com.sap.cds.services.persistence.PersistenceService; +import com.sap.cds.services.request.UserInfo; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.time.Instant; +import java.util.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.*; + +public class SDMAttachmentsServiceHandlerTest { + @Mock private AttachmentCreateEventContext mockContext; + @Mock private AttachmentReadEventContext mockReadContext; + @Mock private List mockData; + @Mock private AuthenticationInfo mockAuthInfo; + @Mock private JwtTokenAuthenticationInfo mockJwtTokenInfo; + private SDMAttachmentsServiceHandler handlerSpy; + private PersistenceService persistenceService; + @Mock private AttachmentMarkAsDeletedEventContext attachmentMarkAsDeletedEventContext; + @Mock private AttachmentRestoreEventContext restoreEventContext; + private SDMService sdmService; + @Mock private CdsModel cdsModel; + + @Mock private CdsEntity cdsEntity; + + @Mock private UserInfo userInfo; + + String objectId = "objectId"; + String folderId = "folderId"; + String userEmail = "email"; + String subdomain = "subdomain"; + JsonObject mockPayload = new JsonObject(); + String token = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTY4MzQxODI4MCwiZXhwIjoxNjg1OTQ0MjgwLCJleHRfYXR0ciI6eyJ6ZG4iOiJ0ZW5hbnQifX0.efgtgCjF7bxG2kEgYbkTObovuZN5YQP5t7yr9aPKntk"; + + @Mock private SDMCredentials sdmCredentials; + @Mock private DeletionUserInfo deletionUserInfo; + + @BeforeEach + public void setUp() { + mockPayload.addProperty("email", "john.doe@example.com"); + mockPayload.addProperty("exp", "1234567890"); + mockPayload.addProperty("zid", "tenant-id-value"); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("zdn", "tenant"); + mockPayload.add("ext_attr", jsonObject); + MockitoAnnotations.openMocks(this); + persistenceService = mock(PersistenceService.class); + sdmService = mock(SDMServiceImpl.class); + when(attachmentMarkAsDeletedEventContext.getContentId()) + .thenReturn("objectId:folderId:entity:subdomain"); + when(attachmentMarkAsDeletedEventContext.getDeletionUserInfo()).thenReturn(deletionUserInfo); + when(deletionUserInfo.getName()).thenReturn(userEmail); + when(mockContext.getUserInfo()).thenReturn(userInfo); + when(userInfo.getName()).thenReturn(userEmail); + handlerSpy = spy(new SDMAttachmentsServiceHandler(persistenceService, sdmService)); + } + + @Test + public void testCreateVersioned() throws IOException { + // Initialization of mocks and setup + Message mockMessage = mock(Message.class); + Messages mockMessages = mock(Messages.class); + MediaData mockMediaData = mock(MediaData.class); + CdsModel mockModel = mock(CdsModel.class); + + when(sdmService.checkRepositoryType(anyString(), any())).thenReturn("Versioned"); + when(mockContext.getMessages()).thenReturn(mockMessages); + when(mockMessages.error("Upload not supported for versioned repositories.")) + .thenReturn(mockMessage); + when(mockContext.getData()).thenReturn(mockMediaData); + when(mockContext.getModel()).thenReturn(mockModel); + when(mockContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); + when(mockAuthInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(mockJwtTokenInfo); + when(mockJwtTokenInfo.getToken()).thenReturn("mockedJwtToken"); + // Use assertThrows to expect a ServiceException and validate the message + ServiceException thrown = + assertThrows( + ServiceException.class, + () -> { + handlerSpy.createAttachment(mockContext); + }); + + // Verify the exception message + assertEquals("Upload not supported for versioned repositories.", thrown.getMessage()); + } + + @Test + public void testCreateNonVersionedDuplicate() throws IOException { + // Initialization of mocks and setup + Map mockattachmentIds = new HashMap<>(); + mockattachmentIds.put("up__ID", "upid"); + mockattachmentIds.put("ID", "id"); + Result mockResult = mock(Result.class); + Row mockRow = mock(Row.class); + List nonEmptyRowList = List.of(mockRow); + MediaData mockMediaData = mock(MediaData.class); + Messages mockMessages = mock(Messages.class); + CdsEntity targetMock = mock(CdsEntity.class); + CdsEntity mockEntity = mock(CdsEntity.class); + CdsEntity mockDraftEntity = mock(CdsEntity.class); + CdsModel mockModel = mock(CdsModel.class); + + when(mockMediaData.getFileName()).thenReturn("sample.pdf"); + when(mockContext.getTarget()).thenReturn(targetMock); + when(targetMock.getQualifiedName()).thenReturn("some.qualified.Name"); + when(mockContext.getModel()).thenReturn(mockModel); + when(mockModel.findEntity("some.qualified.Name.attachments")) + .thenReturn(Optional.of(mockEntity)); + when(mockModel.findEntity("some.qualified.Name.attachments_drafts")) + .thenReturn(Optional.of(mockDraftEntity)); + when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn("Non Versioned"); + when(mockContext.getMessages()).thenReturn(mockMessages); + when(mockContext.getAttachmentIds()).thenReturn(mockattachmentIds); + when(mockContext.getData()).thenReturn(mockMediaData); + doReturn(true).when(handlerSpy).duplicateCheck(any(), any(), any()); + when(mockModel.findEntity(anyString())).thenReturn(Optional.of(mockEntity)); + + try (MockedStatic DBQueryMockedStatic = Mockito.mockStatic(DBQuery.class)) { + when(mockRow.get("columnName")).thenReturn("mockDataValue"); + when(mockResult.list()).thenReturn(nonEmptyRowList); + DBQueryMockedStatic.when( + () -> DBQuery.getAttachmentsForUPID(mockEntity, persistenceService, "upid")) + .thenReturn(mockResult); + when(mockContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); + when(mockAuthInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(mockJwtTokenInfo); + when(mockJwtTokenInfo.getToken()).thenReturn("mockedJwtToken"); + // Use assertThrows to expect a ServiceException and validate the message + ServiceException thrown = + assertThrows( + ServiceException.class, + () -> { + handlerSpy.createAttachment(mockContext); + }); + + // Verify the exception message + assertEquals("sample.pdf already exists.", thrown.getMessage()); + } + } + + @Test + public void testDocumentDeletion() throws IOException { + try (MockedStatic mockedDBQuery = mockStatic(DBQuery.class)) { + + when(attachmentMarkAsDeletedEventContext.getModel()).thenReturn(cdsModel); + when(cdsModel.findEntity(anyString())).thenReturn(Optional.of(cdsEntity)); + List cmisDocuments = new ArrayList<>(); + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setObjectId("objectId1"); + cmisDocuments.add(cmisDocument); + cmisDocument = new CmisDocument(); + cmisDocument.setObjectId("objectId2"); + cmisDocuments.add(cmisDocument); + mockedDBQuery + .when(() -> DBQuery.getAttachmentsForFolder(cdsEntity, persistenceService, folderId)) + .thenReturn(cmisDocuments); + + handlerSpy.markAttachmentAsDeleted(attachmentMarkAsDeletedEventContext); + verify(sdmService).deleteDocument("delete", objectId, userEmail, subdomain); + } + } + + @Test + public void testDocumentDeletionForObjectPresent() throws IOException { + try (MockedStatic mockedDBQuery = mockStatic(DBQuery.class)) { + + when(attachmentMarkAsDeletedEventContext.getModel()).thenReturn(cdsModel); + when(cdsModel.findEntity(anyString())).thenReturn(Optional.of(cdsEntity)); + List cmisDocuments = new ArrayList<>(); + CmisDocument cmisDocument = new CmisDocument(); + cmisDocument.setObjectId("objectId"); + cmisDocuments.add(cmisDocument); + cmisDocument = new CmisDocument(); + cmisDocument.setObjectId("objectId2"); + cmisDocuments.add(cmisDocument); + mockedDBQuery + .when(() -> DBQuery.getAttachmentsForFolder(cdsEntity, persistenceService, folderId)) + .thenReturn(cmisDocuments); + + handlerSpy.markAttachmentAsDeleted(attachmentMarkAsDeletedEventContext); + } + } + + @Test + public void testCreateNonVersionedDIDuplicate() throws IOException { + // Initialization of mocks and setup + Map mockattachmentIds = new HashMap<>(); + mockattachmentIds.put("up__ID", "upid"); + mockattachmentIds.put("ID", "id"); + Result mockResult = mock(Result.class); + Row mockRow = mock(Row.class); + List nonEmptyRowList = List.of(mockRow); + MediaData mockMediaData = + new MediaData() { + @Override + public InputStream getContent() { + return null; + } + + @Override + public void setContent(InputStream inputStream) {} + + @Override + public String getMimeType() { + return null; + } + + @Override + public void setMimeType(String s) {} + + @Override + public String getFileName() { + return "sample.pdf"; + } + + @Override + public void setFileName(String s) {} + + @Override + public String getContentId() { + return null; + } + + @Override + public void setContentId(String s) {} + + @Override + public String getStatus() { + return null; + } + + @Override + public void setStatus(String s) {} + + @Override + public Instant getScannedAt() { + return null; + } + + @Override + public void setScannedAt(Instant instant) {} + + @Override + public Object get(Object o) { + return null; + } + + @Override + public T getPath(String s) { + return null; + } + + @Override + public T getPathOrDefault(String s, T t) { + return null; + } + + @Override + public T putPath(String s, T t) { + return null; + } + + @Override + public T putPathIfAbsent(String s, T t) { + return null; + } + + @Override + public boolean containsPath(String s) { + return false; + } + + @Override + public T removePath(String s) { + return null; + } + + @Override + public T forRemoval(boolean b) { + return null; + } + + @Override + public boolean isForRemoval() { + return false; + } + + @Override + public T getMetadata(String s) { + return null; + } + + @Override + public T putMetadata(String s, T t) { + return null; + } + + @Override + public String toJson() { + return null; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean containsKey(Object key) { + return false; + } + + @Override + public boolean containsValue(Object value) { + return false; + } + + @Nullable + @Override + public Object put(String key, Object value) { + return null; + } + + @Override + public Object remove(Object key) { + return null; + } + + @Override + public void putAll(@NotNull Map m) {} + + @Override + public void clear() {} + + @NotNull + @Override + public Set keySet() { + return null; + } + + @NotNull + @Override + public Collection values() { + return null; + } + + @NotNull + @Override + public Set> entrySet() { + return null; + } + }; + Messages mockMessages = mock(Messages.class); + CdsEntity targetMock = mock(CdsEntity.class); + CdsEntity mockEntity = mock(CdsEntity.class); + CdsEntity mockDraftEntity = mock(CdsEntity.class); + CdsModel mockModel = mock(CdsModel.class); + byte[] byteArray = "Example content".getBytes(); + InputStream contentStream = new ByteArrayInputStream(byteArray); + JSONObject mockCreateResult = new JSONObject(); + mockCreateResult.put("status", "duplicate"); + mockCreateResult.put("name", "sample.pdf"); + // when(mockMediaData.getContent()).thenReturn(contentStream); + when(mockContext.getTarget()).thenReturn(targetMock); + when(targetMock.getQualifiedName()).thenReturn("some.qualified.Name"); + when(mockContext.getModel()).thenReturn(mockModel); + when(mockModel.findEntity("some.qualified.Name.attachments")) + .thenReturn(Optional.of(mockEntity)); + when(mockModel.findEntity("some.qualified.Name.attachments_drafts")) + .thenReturn(Optional.of(mockDraftEntity)); + when(sdmService.checkRepositoryType(SDMConstants.REPOSITORY_ID, token)) + .thenReturn("Non Versioned"); + when(mockContext.getMessages()).thenReturn(mockMessages); + when(mockContext.getAttachmentIds()).thenReturn(mockattachmentIds); + when(mockContext.getData()).thenReturn(mockMediaData); + doReturn(false).when(handlerSpy).duplicateCheck(any(), any(), any()); + when(mockModel.findEntity(anyString())).thenReturn(Optional.of(mockEntity)); + when(mockContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); + when(mockAuthInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(mockJwtTokenInfo); + when(mockJwtTokenInfo.getToken()).thenReturn("mockedJwtToken"); + when(sdmService.getFolderId(any(), any(), any(), any())).thenReturn("folderid"); + when(sdmService.createDocument(any(), any(), any())).thenReturn(mockCreateResult); + + try (MockedStatic DBQueryMockedStatic = Mockito.mockStatic(DBQuery.class); + MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + when(mockRow.get("columnName")).thenReturn("mockDataValue"); + when(mockResult.list()).thenReturn(nonEmptyRowList); + DBQueryMockedStatic.when( + () -> DBQuery.getAttachmentsForUPID(mockEntity, persistenceService, "upid")) + .thenReturn(mockResult); + SDMCredentials mockSdmCredentials = Mockito.mock(SDMCredentials.class); + + tokenHandlerMockedStatic.when(TokenHandler::getSDMCredentials).thenReturn(mockSdmCredentials); + Mockito.when(TokenHandler.getTokenFields(anyString())).thenReturn(mockPayload); + + // Use assertThrows to expect a ServiceException and validate the message + ServiceException thrown = + assertThrows( + ServiceException.class, + () -> { + handlerSpy.createAttachment(mockContext); + }); + + assertEquals("sample.pdf already exists.", thrown.getMessage()); + + // Add any additional verifications if needed + } + } + + @Test + public void testFolderDeletion() throws IOException { + try (MockedStatic mockedDBQuery = mockStatic(DBQuery.class)) { + + when(attachmentMarkAsDeletedEventContext.getModel()).thenReturn(cdsModel); + when(cdsModel.findEntity(anyString())).thenReturn(Optional.of(cdsEntity)); + List cmisDocuments = new ArrayList<>(); + mockedDBQuery + .when(() -> DBQuery.getAttachmentsForFolder(cdsEntity, persistenceService, folderId)) + .thenReturn(cmisDocuments); + handlerSpy.markAttachmentAsDeleted(attachmentMarkAsDeletedEventContext); + verify(sdmService).deleteDocument("deleteTree", folderId, userEmail, subdomain); + } + } + + @Test + public void testCreateNonVersionedDIVirus() throws IOException { + // Initialization of mocks and setup + Map mockattachmentIds = new HashMap<>(); + mockattachmentIds.put("up__ID", "upid"); + mockattachmentIds.put("ID", "id"); + Result mockResult = mock(Result.class); + Row mockRow = mock(Row.class); + List nonEmptyRowList = List.of(mockRow); + MediaData mockMediaData = mock(MediaData.class); + Messages mockMessages = mock(Messages.class); + CdsEntity targetMock = mock(CdsEntity.class); + CdsEntity mockEntity = mock(CdsEntity.class); + CdsEntity mockDraftEntity = mock(CdsEntity.class); + CdsModel mockModel = mock(CdsModel.class); + byte[] byteArray = "Example content".getBytes(); + InputStream contentStream = new ByteArrayInputStream(byteArray); + JSONObject mockCreateResult = new JSONObject(); + mockCreateResult.put("status", "virus"); + mockCreateResult.put("name", "sample.pdf"); + + when(mockMediaData.getFileName()).thenReturn("sample.pdf"); + when(mockMediaData.getContent()).thenReturn(contentStream); + when(mockContext.getTarget()).thenReturn(targetMock); + when(targetMock.getQualifiedName()).thenReturn("some.qualified.Name"); + when(mockContext.getModel()).thenReturn(mockModel); + when(mockModel.findEntity("some.qualified.Name.attachments")) + .thenReturn(Optional.of(mockEntity)); + when(mockModel.findEntity("some.qualified.Name.attachments_drafts")) + .thenReturn(Optional.of(mockDraftEntity)); + when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn("Non Versioned"); + when(mockContext.getMessages()).thenReturn(mockMessages); + when(mockContext.getAttachmentIds()).thenReturn(mockattachmentIds); + when(mockContext.getData()).thenReturn(mockMediaData); + doReturn(false).when(handlerSpy).duplicateCheck(any(), any(), any()); + when(mockModel.findEntity(anyString())).thenReturn(Optional.of(mockEntity)); + when(mockContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); + when(mockAuthInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(mockJwtTokenInfo); + when(mockJwtTokenInfo.getToken()).thenReturn("mockedJwtToken"); + when(sdmService.getFolderId(any(), any(), any(), any())).thenReturn("folderid"); + when(sdmService.createDocument(any(), any(), any())).thenReturn(mockCreateResult); + + try (MockedStatic DBQueryMockedStatic = Mockito.mockStatic(DBQuery.class); + MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + when(mockRow.get("columnName")).thenReturn("mockDataValue"); + when(mockResult.list()).thenReturn(nonEmptyRowList); + DBQueryMockedStatic.when( + () -> DBQuery.getAttachmentsForUPID(mockEntity, persistenceService, "upid")) + .thenReturn(mockResult); + SDMCredentials mockSdmCredentials = Mockito.mock(SDMCredentials.class); + + tokenHandlerMockedStatic.when(TokenHandler::getSDMCredentials).thenReturn(mockSdmCredentials); + Mockito.when(TokenHandler.getTokenFields(anyString())).thenReturn(mockPayload); + + // Use assertThrows to expect a ServiceException and validate the message + ServiceException thrown = + assertThrows( + ServiceException.class, + () -> { + handlerSpy.createAttachment(mockContext); + }); + + // Verify the exception message + assertEquals( + "sample.pdf contains potential malware and cannot be uploaded.", thrown.getMessage()); + } + } + + @Test + public void testCreateNonVersionedDIOther() throws IOException { + // Initialization of mocks and setup + Map mockattachmentIds = new HashMap<>(); + mockattachmentIds.put("up__ID", "upid"); + mockattachmentIds.put("ID", "id"); + Result mockResult = mock(Result.class); + Row mockRow = mock(Row.class); + List nonEmptyRowList = List.of(mockRow); + MediaData mockMediaData = mock(MediaData.class); + Messages mockMessages = mock(Messages.class); + CdsEntity targetMock = mock(CdsEntity.class); + CdsEntity mockEntity = mock(CdsEntity.class); + CdsEntity mockDraftEntity = mock(CdsEntity.class); + CdsModel mockModel = mock(CdsModel.class); + byte[] byteArray = "Example content".getBytes(); + InputStream contentStream = new ByteArrayInputStream(byteArray); + JSONObject mockCreateResult = new JSONObject(); + mockCreateResult.put("status", "fail"); + mockCreateResult.put("message", "Failed due to a DI error"); + mockCreateResult.put("name", "sample.pdf"); + + when(mockMediaData.getFileName()).thenReturn("sample.pdf"); + when(mockMediaData.getContent()).thenReturn(contentStream); + when(mockContext.getTarget()).thenReturn(targetMock); + when(targetMock.getQualifiedName()).thenReturn("some.qualified.Name"); + when(mockContext.getModel()).thenReturn(mockModel); + when(mockModel.findEntity("some.qualified.Name.attachments")) + .thenReturn(Optional.of(mockEntity)); + when(mockModel.findEntity("some.qualified.Name.attachments_drafts")) + .thenReturn(Optional.of(mockDraftEntity)); + when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn("Non Versioned"); + when(mockContext.getMessages()).thenReturn(mockMessages); + when(mockContext.getAttachmentIds()).thenReturn(mockattachmentIds); + when(mockContext.getData()).thenReturn(mockMediaData); + doReturn(false).when(handlerSpy).duplicateCheck(any(), any(), any()); + when(mockModel.findEntity(anyString())).thenReturn(Optional.of(mockEntity)); + when(mockContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); + when(mockAuthInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(mockJwtTokenInfo); + when(mockJwtTokenInfo.getToken()).thenReturn("mockedJwtToken"); + when(sdmService.getFolderId(any(), any(), any(), any())).thenReturn("folderid"); + when(sdmService.createDocument(any(), any(), any())).thenReturn(mockCreateResult); + + try (MockedStatic DBQueryMockedStatic = Mockito.mockStatic(DBQuery.class); + MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + when(mockRow.get("columnName")).thenReturn("mockDataValue"); + when(mockResult.list()).thenReturn(nonEmptyRowList); + DBQueryMockedStatic.when( + () -> DBQuery.getAttachmentsForUPID(mockEntity, persistenceService, "upid")) + .thenReturn(mockResult); + SDMCredentials mockSdmCredentials = Mockito.mock(SDMCredentials.class); + + tokenHandlerMockedStatic.when(TokenHandler::getSDMCredentials).thenReturn(mockSdmCredentials); + Mockito.when(TokenHandler.getTokenFields(anyString())).thenReturn(mockPayload); + + // Use assertThrows to expect a ServiceException and validate the message + ServiceException thrown = + assertThrows( + ServiceException.class, + () -> { + handlerSpy.createAttachment(mockContext); + }); + + // Verify the exception message + assertEquals("Failed due to a DI error", thrown.getMessage()); + } + } + + @Test + public void testCreateNonVersionedDISuccess() throws IOException { + Map mockattachmentIds = new HashMap<>(); + mockattachmentIds.put("up__ID", "upid"); + mockattachmentIds.put("ID", "id"); + Result mockResult = mock(Result.class); + MediaData mockMediaData = mock(MediaData.class); + Messages mockMessages = mock(Messages.class); + CdsEntity targetMock = mock(CdsEntity.class); + CdsEntity mockEntity = mock(CdsEntity.class); + CdsEntity mockDraftEntity = mock(CdsEntity.class); + CdsModel mockModel = mock(CdsModel.class); + byte[] byteArray = "Example content".getBytes(); + InputStream contentStream = new ByteArrayInputStream(byteArray); + JSONObject mockCreateResult = new JSONObject(); + mockCreateResult.put("status", "success"); + mockCreateResult.put("url", "url"); + mockCreateResult.put("name", "sample.pdf"); + + when(mockMediaData.getFileName()).thenReturn("sample.pdf"); + when(mockMediaData.getContent()).thenReturn(contentStream); + when(mockContext.getTarget()).thenReturn(targetMock); + when(targetMock.getQualifiedName()).thenReturn("some.qualified.Name"); + when(mockContext.getModel()).thenReturn(mockModel); + when(mockModel.findEntity("some.qualified.Name.attachments")) + .thenReturn(Optional.of(mockEntity)); + when(mockModel.findEntity("some.qualified.Name.attachments_drafts")) + .thenReturn(Optional.of(mockDraftEntity)); + when(sdmService.checkRepositoryType(anyString(), anyString())).thenReturn("Non Versioned"); + when(mockContext.getMessages()).thenReturn(mockMessages); + when(mockContext.getAttachmentIds()).thenReturn(mockattachmentIds); + when(mockContext.getData()).thenReturn(mockMediaData); + doReturn(false).when(handlerSpy).duplicateCheck(any(), any(), any()); + when(mockModel.findEntity(anyString())).thenReturn(Optional.of(mockEntity)); + when(mockContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); + when(mockAuthInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(mockJwtTokenInfo); + when(mockJwtTokenInfo.getToken()).thenReturn("mockedJwtToken"); + when(sdmService.getFolderId(any(), any(), any(), any())).thenReturn("folderid"); + when(sdmService.createDocument(any(), any(), any())).thenReturn(mockCreateResult); + + try (MockedStatic DBQueryMockedStatic = Mockito.mockStatic(DBQuery.class); + MockedStatic tokenHandlerMockedStatic = + Mockito.mockStatic(TokenHandler.class)) { + DBQueryMockedStatic.when( + () -> DBQuery.getAttachmentsForUPID(mockEntity, persistenceService, "upid")) + .thenReturn(mockResult); + SDMCredentials mockSdmCredentials = Mockito.mock(SDMCredentials.class); + + tokenHandlerMockedStatic.when(TokenHandler::getSDMCredentials).thenReturn(mockSdmCredentials); + handlerSpy.createAttachment(mockContext); + verifyNoInteractions(mockMessages); + } + } + + @Test + void testDuplicateCheck_NoDuplicates() { + Result result = mock(Result.class); + + // Mocking a raw list of maps + List mockedResultList = new ArrayList<>(); + Map map1 = new HashMap<>(); + map1.put("key1", "value1"); + mockedResultList.add(map1); + + // Casting to raw types to avoid type mismatch + when(result.listOf(Map.class)).thenReturn((List) mockedResultList); + + String filename = "sample.pdf"; + String fileid = "123"; + Map attachment = new HashMap<>(); + attachment.put("fileName", filename); + attachment.put("ID", fileid); + + List resultList = Arrays.asList((Map) attachment); + when(result.listOf(Map.class)).thenReturn((List) resultList); + + boolean isDuplicate = handlerSpy.duplicateCheck(filename, fileid, result); + assertFalse(isDuplicate, "Expected no duplicates"); + } + + @Test + void testDuplicateCheck_WithDuplicate() { + Result result = mock(Result.class); + + // Mocking a raw list of maps + List mockedResultList = new ArrayList<>(); + + // Creating a map with duplicate filename but different file ID + Map attachment1 = new HashMap<>(); + attachment1.put("fileName", "sample.pdf"); + attachment1.put("ID", "123"); // Different ID, not a duplicate + + Map attachment2 = new HashMap<>(); + attachment2.put("fileName", "sample.pdf"); + attachment2.put("ID", "456"); // Same filename but different ID (this is the duplicate) + + mockedResultList.add((Map) attachment1); + mockedResultList.add((Map) attachment2); + + // Mocking the result to return the list containing the attachments + when(result.listOf(Map.class)).thenReturn((List) mockedResultList); + + String filename = "sample.pdf"; + String fileid = "123"; // The fileid to check, same as attachment1, different from attachment2 + + // Checking for duplicate + boolean isDuplicate = handlerSpy.duplicateCheck(filename, fileid, result); + + // Assert that a duplicate is found + assertTrue(isDuplicate, "Expected to find a duplicate"); + } + + @Test + public void testReadAttachment_NotVersionedRepository() throws IOException { + when(mockReadContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); + when(mockAuthInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(mockJwtTokenInfo); + when(mockJwtTokenInfo.getToken()).thenReturn("dummyToken"); + when(mockReadContext.getContentId()).thenReturn("objectId:part2"); + try (MockedStatic mockedStatic = mockStatic(TokenHandler.class)) { + when(sdmService.checkRepositoryType(SDMConstants.REPOSITORY_ID, token)) + .thenReturn("NotVersioned"); + when(TokenHandler.getSDMCredentials()).thenReturn(sdmCredentials); + + handlerSpy.readAttachment(mockReadContext); + + // Verify that readDocument method was called + verify(sdmService) + .readDocument(anyString(), anyString(), any(SDMCredentials.class), eq(mockReadContext)); + } + } + + @Test + public void testReadAttachment_FailureInReadDocument() throws IOException { + when(mockReadContext.getAuthenticationInfo()).thenReturn(mockAuthInfo); + when(mockAuthInfo.as(JwtTokenAuthenticationInfo.class)).thenReturn(mockJwtTokenInfo); + when(mockJwtTokenInfo.getToken()).thenReturn("dummyToken"); + when(mockReadContext.getContentId()).thenReturn("objectId:part2"); + try (MockedStatic mockedStatic = mockStatic(TokenHandler.class)) { + when(sdmService.checkRepositoryType(SDMConstants.REPOSITORY_ID, token)) + .thenReturn("NotVersioned"); + when(TokenHandler.getSDMCredentials()).thenReturn(sdmCredentials); + doThrow(new ServiceException(SDMConstants.FILE_NOT_FOUND_ERROR)) + .when(sdmService) + .readDocument(anyString(), anyString(), any(SDMCredentials.class), eq(mockReadContext)); + + ServiceException exception = + assertThrows( + ServiceException.class, + () -> { + handlerSpy.readAttachment(mockReadContext); + }); + + assertEquals("Object not found in repository", exception.getMessage()); + } + } + + @Test + public void testRestoreAttachment() { + handlerSpy.restoreAttachment(restoreEventContext); + } +} diff --git a/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java new file mode 100644 index 00000000..2dd6c46a --- /dev/null +++ b/sdm/src/test/java/unit/com/sap/cds/sdm/utilities/SDMUtilsTest.java @@ -0,0 +1,78 @@ +package unit.com.sap.cds.sdm.utilities; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.sap.cds.CdsData; +import com.sap.cds.sdm.utilities.SDMUtils; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class SDMUtilsTest { + + @Test + public void testIsFileNameDuplicateInDrafts() { + List data = new ArrayList<>(); + CdsData mockCdsData = mock(CdsData.class); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + Map attachment1 = new HashMap<>(); + attachment1.put("fileName", "file1.txt"); + Map attachment2 = new HashMap<>(); + attachment2.put("fileName", "file1.txt"); + attachments.add(attachment1); + attachments.add(attachment2); + entity.put("attachments", attachments); + when(mockCdsData.get("attachments")).thenReturn(attachments); // Correctly mock get method + data.add(mockCdsData); + + Set duplicateFilenames = SDMUtils.isFileNameDuplicateInDrafts(data); + + assertTrue(duplicateFilenames.contains("file1.txt")); + } + + @Test + public void testIsFileNameContainsRestrictedCharaters() { + List data = new ArrayList<>(); + CdsData mockCdsData = mock(CdsData.class); + Map entity = new HashMap<>(); + List> attachments = new ArrayList<>(); + + Map attachment1 = new HashMap<>(); + attachment1.put("fileName", "file1.txt"); + Map attachment2 = new HashMap<>(); + attachment2.put("fileName", "file2/abc.txt"); + Map attachment3 = new HashMap<>(); + attachment3.put("fileName", "file3\\abc.txt"); + attachments.add(attachment1); + attachments.add(attachment2); + attachments.add(attachment3); + entity.put("attachments", attachments); + when(mockCdsData.get("attachments")).thenReturn(attachments); // Correctly mock get method + data.add(mockCdsData); + + List restrictedFilenames = SDMUtils.isFileNameContainsRestrictedCharaters(data); + + assertEquals(2, restrictedFilenames.size()); + assertTrue(restrictedFilenames.contains("file2/abc.txt")); + assertTrue(restrictedFilenames.contains("file3\\abc.txt")); + } + + @Test + public void testIsRestrictedCharactersInName() { + assertTrue(SDMUtils.isRestrictedCharactersInName("file/abc.txt")); + assertTrue(SDMUtils.isRestrictedCharactersInName("file\\abc.txt")); + assertFalse(SDMUtils.isRestrictedCharactersInName("file-abc.txt")); + assertFalse(SDMUtils.isRestrictedCharactersInName("file_abc.txt")); + } +} diff --git a/sdm/src/test/resources/credentials.properties b/sdm/src/test/resources/credentials.properties new file mode 100644 index 00000000..10ad65d5 --- /dev/null +++ b/sdm/src/test/resources/credentials.properties @@ -0,0 +1,6 @@ +appUrl= +authUrl= +clientID= +clientSecret= +username= +password= \ No newline at end of file diff --git a/sdm/src/test/resources/sample.exe b/sdm/src/test/resources/sample.exe new file mode 100644 index 00000000..e69de29b diff --git a/sdm/src/test/resources/sample.pdf b/sdm/src/test/resources/sample.pdf new file mode 100644 index 00000000..fadf9f86 Binary files /dev/null and b/sdm/src/test/resources/sample.pdf differ diff --git a/sdm/src/test/resources/sample.txt b/sdm/src/test/resources/sample.txt new file mode 100644 index 00000000..e69de29b diff --git a/sdm/src/test/resources/sample1.pdf b/sdm/src/test/resources/sample1.pdf new file mode 100644 index 00000000..fadf9f86 Binary files /dev/null and b/sdm/src/test/resources/sample1.pdf differ diff --git a/sdm/src/test/resources/sample2.pdf b/sdm/src/test/resources/sample2.pdf new file mode 100644 index 00000000..fadf9f86 Binary files /dev/null and b/sdm/src/test/resources/sample2.pdf differ