diff --git a/.github/workflows/multi tenancy_Integration.yml b/.github/workflows/multi tenancy_Integration.yml index 060a5409..9320595f 100644 --- a/.github/workflows/multi tenancy_Integration.yml +++ b/.github/workflows/multi tenancy_Integration.yml @@ -349,12 +349,323 @@ jobs: sdm/target/failsafe-reports/ retention-days: 7 + # Active-entity tests run serially in their own slot AFTER integration-test to avoid + # fixture-collision with sibling matrix entries (all touch the same Books/author='author' rows). + active-entity-test: + environment: dev + runs-on: ubuntu-latest + needs: integration-test + strategy: + fail-fast: false + matrix: + tokenFlow: [namedUser, technicalUser] + tenant: [TENANT1, TENANT2] + testClass: + - IntegrationTest_ActiveEntity + - IntegrationTest_ActiveEntity_MultipleFacet + - IntegrationTest_ActiveEntity_Chapters_MultipleFacet + + steps: + - name: Checkout repository โœ… + uses: actions/checkout@v6 + + - name: Set up Java 21 โ˜• + uses: actions/setup-java@v3 + with: + java-version: 21 + distribution: 'temurin' + cache: 'maven' + + - name: Cache CF CLI ๐Ÿ“ฆ + id: cache-cf-cli + uses: actions/cache@v4 + with: + path: /usr/bin/cf8 + key: cf-cli-v8-${{ runner.os }} + + - name: Install Cloud Foundry CLI and jq ๐Ÿ“ฆ + if: steps.cache-cf-cli.outputs.cache-hit != 'true' + run: | + echo "๐Ÿ”ง Installing Cloud Foundry CLI..." + 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 + + - name: Install jq ๐Ÿ“ฆ + run: | + if ! command -v jq &> /dev/null; then + sudo apt-get update && sudo apt-get install -y jq + fi + + + - name: Login to Cloud Foundry ๐Ÿ”‘ + env: + CF_API: ${{ secrets.CF_API }} + CF_USER: ${{ secrets.CF_USER }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD }} + CF_ORG: ${{ secrets.CF_ORG }} + CF_SPACE: ${{ secrets.CF_SPACE }} + run: | + set +x + echo "::add-mask::$CF_API" + echo "::add-mask::$CF_USER" + echo "::add-mask::$CF_PASSWORD" + echo "::add-mask::$CF_ORG" + echo "::add-mask::$CF_SPACE" + echo "๐Ÿ”„ Logging in to Cloud Foundry using space: $CF_SPACE" + cf login -a "$CF_API" \ + -u "$CF_USER" \ + -p "$CF_PASSWORD" \ + -o "$CF_ORG" \ + -s $CF_SPACE > /dev/null + + - name: Fetch and Escape Client Details for single tenant ๐Ÿ” + id: fetch_credentials + run: | + echo "Fetching client details for single tenant..." + 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 + + 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 + + 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 + escapedClientSecret=$(echo "$clientSecret" | sed 's/\$/\\$/g') + echo "::add-mask::$escapedClientSecret" + + clientID=$(echo "$binding_details" | jq -r '.credentials.clientid') + if [ -z "$clientID" ] || [ "$clientID" == "null" ]; then + echo "โŒ Error: clientID is not set or is null"; exit 1; + fi + echo "::add-mask::$clientID" + + echo "CLIENT_SECRET=$escapedClientSecret" >> $GITHUB_OUTPUT + echo "CLIENT_ID=$clientID" >> $GITHUB_OUTPUT + echo "โœ… Client details fetched successfully!" + + - name: Fetch and Escape Client Details for multi tenant ๐Ÿ” + id: fetch_credentials_mt + run: | + echo "Fetching client details for multi tenant..." + service_instance_guid=$(cf service bookshop-mt-uaa --guid) + if [ -z "$service_instance_guid" ]; then + echo "โŒ Error: Unable to retrieve service instance GUID"; exit 1; + fi + + 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 + + binding_details=$(cf curl "/v3/service_credential_bindings/${binding_guid}/details") + + clientSecret_mt=$(echo "$binding_details" | jq -r '.credentials.clientsecret') + if [ -z "$clientSecret_mt" ] || [ "$clientSecret_mt" == "null" ]; then + echo "โŒ Error: clientSecret_mt is not set or is null"; exit 1; + fi + escapedClientSecret_mt=$(echo "$clientSecret_mt" | sed 's/\$/\\$/g') + echo "::add-mask::$escapedClientSecret_mt" + + clientID_mt=$(echo "$binding_details" | jq -r '.credentials.clientid') + if [ -z "$clientID_mt" ] || [ "$clientID_mt" == "null" ]; then + echo "โŒ Error: clientID_mt is not set or is null"; exit 1; + fi + echo "::add-mask::$clientID_mt" + + echo "CLIENT_SECRET_MT=$escapedClientSecret_mt" >> $GITHUB_OUTPUT + echo "CLIENT_ID_MT=$clientID_mt" >> $GITHUB_OUTPUT + echo "โœ… Multi-tenant client details fetched successfully!" + + - name: Fetch and Escape SDM CMIS Credentials ๐Ÿ” + id: fetch_credentials_cmis + run: | + echo "๐Ÿ”„ Fetching SDM CMIS credentials from CF service binding..." + service_instance_guid=$(cf service sdm --guid) + if [ -z "$service_instance_guid" ]; then + echo "โŒ Error: Unable to retrieve SDM service instance GUID"; exit 1; + fi + + bindings_response=$(cf curl "/v3/service_credential_bindings?service_instance_guids=${service_instance_guid}") + app_guid=$(cf app bookshop-mt-srv --guid) + binding_guid=$(echo "$bindings_response" | jq -r \ + --arg app_guid "$app_guid" \ + '.resources[] | select(.relationships.app.data.guid == $app_guid) | .guid' | head -1) + if [ -z "$binding_guid" ]; then + echo "โŒ Error: Unable to retrieve SDM binding GUID for bookshop-mt-srv"; exit 1; + fi + + binding_details=$(cf curl "/v3/service_credential_bindings/${binding_guid}/details") + + cmis_client_secret=$(echo "$binding_details" | jq -r '.credentials.uaa.clientsecret // .credentials.clientsecret // empty') + if [ -z "$cmis_client_secret" ] || [ "$cmis_client_secret" == "null" ]; then + echo "โŒ Error: SDM clientsecret is not set or is null"; exit 1; + fi + echo "::add-mask::$cmis_client_secret" + + cmis_client_id=$(echo "$binding_details" | jq -r '.credentials.uaa.clientid // .credentials.clientid // empty') + if [ -z "$cmis_client_id" ] || [ "$cmis_client_id" == "null" ]; then + echo "โŒ Error: SDM clientid is not set or is null"; exit 1; + fi + echo "::add-mask::$cmis_client_id" + + cmis_url=$(echo "$binding_details" | jq -r '.credentials.uri // .credentials.endpoints.ecm_service // .credentials.url // empty') + if [ -z "$cmis_url" ]; then + echo "โŒ Error: SDM CMIS URL not found in binding details"; exit 1; + fi + echo "::add-mask::$cmis_url" + + printf 'CMIS_CLIENT_SECRET=%s\n' "$cmis_client_secret" >> $GITHUB_OUTPUT + printf 'CMIS_CLIENT_ID=%s\n' "$cmis_client_id" >> $GITHUB_OUTPUT + printf 'CMIS_URL=%s\n' "$cmis_url" >> $GITHUB_OUTPUT + echo "โœ… SDM CMIS credentials fetched successfully!" + + - name: Prepare credentials file ๐Ÿ“ + env: + CLIENT_SECRET: ${{ steps.fetch_credentials.outputs.CLIENT_SECRET }} + CLIENT_ID: ${{ steps.fetch_credentials.outputs.CLIENT_ID }} + CLIENT_SECRET_MT: ${{ steps.fetch_credentials_mt.outputs.CLIENT_SECRET_MT }} + CLIENT_ID_MT: ${{ steps.fetch_credentials_mt.outputs.CLIENT_ID_MT }} + CMIS_CLIENT_ID: ${{ steps.fetch_credentials_cmis.outputs.CMIS_CLIENT_ID }} + CMIS_CLIENT_SECRET: ${{ steps.fetch_credentials_cmis.outputs.CMIS_CLIENT_SECRET }} + CMIS_URL_FROM_CF: ${{ steps.fetch_credentials_cmis.outputs.CMIS_URL }} + CF_ORG: ${{ secrets.CF_ORG }} + CAPAUTH_URL: ${{ secrets.CAPAUTH_URL }} + AUTHURLMT1: ${{ secrets.AUTHURLMT1 }} + AUTHURLMT2: ${{ secrets.AUTHURLMT2 }} + CF_USER: ${{ secrets.CF_USER }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD }} + NOSDMROLEUSERNAME: ${{ secrets.NOSDMROLEUSERNAME }} + NOSDMROLEUSERPASSWORD: ${{ secrets.NOSDMROLEUSERPASSWORD }} + DEFAULTREPOSITORYID: ${{ secrets.DEFAULTREPOSITORYID }} + DEFAULTREPOSITORYIDMT: ${{ secrets.DEFAULTREPOSITORYIDMT }} + CF_SPACE: ${{ secrets.CF_SPACE }} + run: | + echo "๐Ÿš€ Preparing credentials for ${{ matrix.tokenFlow }} - ${{ matrix.tenant }}..." + set +x + set -e + PROPERTIES_FILE="sdm/src/test/resources/credentials.properties" + appUrl="$CF_ORG-$CF_SPACE-demoappjava-srv.cfapps.eu12.hana.ondemand.com" + appUrlMT="$CF_ORG-$CF_SPACE-bookshop-mt-srv.cfapps.eu12.hana.ondemand.com" + authUrl="$CAPAUTH_URL" + authUrlMT1="$AUTHURLMT1" + authUrlMT2="$AUTHURLMT2" + clientID="$CLIENT_ID" + clientSecret="$CLIENT_SECRET" + clientIDMT="$CLIENT_ID_MT" + clientSecretMT="$CLIENT_SECRET_MT" + username="$CF_USER" + password="$CF_PASSWORD" + noSDMRoleUsername="$NOSDMROLEUSERNAME" + noSDMRoleUserPassword="$NOSDMROLEUSERPASSWORD" + CMIS_URL="${{ env.CMIS_URL_FROM_CF }}" + cmisClientID="$CMIS_CLIENT_ID" + cmisClientSecret="$CMIS_CLIENT_SECRET" + defaultRepositoryID="$DEFAULTREPOSITORYID" + defaultRepositoryIDMT="$DEFAULTREPOSITORYIDMT" + + echo "::add-mask::$clientSecret" + echo "::add-mask::$clientID" + echo "::add-mask::$clientSecretMT" + echo "::add-mask::$clientIDMT" + echo "::add-mask::$username" + echo "::add-mask::$password" + echo "::add-mask::$noSDMRoleUsername" + echo "::add-mask::$noSDMRoleUserPassword" + echo "::add-mask::$CMIS_URL" + echo "::add-mask::$cmisClientID" + echo "::add-mask::$cmisClientSecret" + + if [ -z "$appUrl" ]; then echo "โŒ Error: appUrl is not set"; exit 1; fi + if [ -z "$appUrlMT" ]; then echo "โŒ Error: appUrlMT is not set"; exit 1; fi + if [ -z "$authUrl" ]; then echo "โŒ Error: authUrl is not set"; exit 1; fi + if [ -z "$authUrlMT1" ]; then echo "โŒ Error: authUrlMT1 is not set"; exit 1; fi + if [ -z "$authUrlMT2" ]; then echo "โŒ Error: authUrlMT2 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 "$clientIDMT" ]; then echo "โŒ Error: clientIDMT is not set"; exit 1; fi + if [ -z "$clientSecretMT" ]; then echo "โŒ Error: clientSecretMT 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 + if [ -z "$noSDMRoleUsername" ]; then echo "โŒ Error: noSDMRoleUsername is not set"; exit 1; fi + if [ -z "$noSDMRoleUserPassword" ]; then echo "โŒ Error: noSDMRoleUserPassword is not set"; exit 1; fi + if [ -z "$CMIS_URL" ]; then echo "โŒ Error: CMIS_URL is not set"; exit 1; fi + if [ -z "$cmisClientID" ]; then echo "โŒ Error: cmisClientID is not set"; exit 1; fi + if [ -z "$cmisClientSecret" ]; then echo "โŒ Error: cmisClientSecret is not set"; exit 1; fi + + cat > "$PROPERTIES_FILE" < /dev/null; then + sudo apt-get update && sudo apt-get install -y jq + fi + + + - name: Login to Cloud Foundry ๐Ÿ”‘ + env: + CF_API: ${{ secrets.CF_API }} + CF_USER: ${{ secrets.CF_USER }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD }} + CF_ORG: ${{ secrets.CF_ORG }} + CF_SPACE: ${{ secrets.CF_SPACE }} + run: | + set +x + echo "::add-mask::$CF_API" + echo "::add-mask::$CF_USER" + echo "::add-mask::$CF_PASSWORD" + echo "::add-mask::$CF_ORG" + echo "::add-mask::$CF_SPACE" + echo "๐Ÿ”„ Logging in to Cloud Foundry using space: $CF_SPACE" + cf login -a "$CF_API" \ + -u "$CF_USER" \ + -p "$CF_PASSWORD" \ + -o "$CF_ORG" \ + -s $CF_SPACE > /dev/null + + - name: Fetch and Escape Client Details for single tenant ๐Ÿ” + id: fetch_credentials + run: | + echo "Fetching client details for single tenant..." + 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 + + 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 + + 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 + escapedClientSecret=$(echo "$clientSecret" | sed 's/\$/\\$/g') + echo "::add-mask::$escapedClientSecret" + + clientID=$(echo "$binding_details" | jq -r '.credentials.clientid') + if [ -z "$clientID" ] || [ "$clientID" == "null" ]; then + echo "โŒ Error: clientID is not set or is null"; exit 1; + fi + echo "::add-mask::$clientID" + + echo "CLIENT_SECRET=$escapedClientSecret" >> $GITHUB_OUTPUT + echo "CLIENT_ID=$clientID" >> $GITHUB_OUTPUT + echo "โœ… Client details fetched successfully!" + + - name: Fetch and Escape Client Details for multi tenant ๐Ÿ” + id: fetch_credentials_mt + run: | + echo "Fetching client details for multi tenant..." + service_instance_guid=$(cf service bookshop-mt-uaa --guid) + if [ -z "$service_instance_guid" ]; then + echo "โŒ Error: Unable to retrieve service instance GUID"; exit 1; + fi + + 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 + + binding_details=$(cf curl "/v3/service_credential_bindings/${binding_guid}/details") + + clientSecret_mt=$(echo "$binding_details" | jq -r '.credentials.clientsecret') + if [ -z "$clientSecret_mt" ] || [ "$clientSecret_mt" == "null" ]; then + echo "โŒ Error: clientSecret_mt is not set or is null"; exit 1; + fi + escapedClientSecret_mt=$(echo "$clientSecret_mt" | sed 's/\$/\\$/g') + echo "::add-mask::$escapedClientSecret_mt" + + clientID_mt=$(echo "$binding_details" | jq -r '.credentials.clientid') + if [ -z "$clientID_mt" ] || [ "$clientID_mt" == "null" ]; then + echo "โŒ Error: clientID_mt is not set or is null"; exit 1; + fi + echo "::add-mask::$clientID_mt" + + echo "CLIENT_SECRET_MT=$escapedClientSecret_mt" >> $GITHUB_OUTPUT + echo "CLIENT_ID_MT=$clientID_mt" >> $GITHUB_OUTPUT + echo "โœ… Multi-tenant client details fetched successfully!" + + - name: Fetch and Escape SDM CMIS Credentials ๐Ÿ” + id: fetch_credentials_cmis + run: | + echo "๐Ÿ”„ Fetching SDM CMIS credentials from CF service binding..." + service_instance_guid=$(cf service sdm --guid) + if [ -z "$service_instance_guid" ]; then + echo "โŒ Error: Unable to retrieve SDM service instance GUID"; exit 1; + fi + + bindings_response=$(cf curl "/v3/service_credential_bindings?service_instance_guids=${service_instance_guid}") + app_guid=$(cf app bookshop-mt-srv --guid) + binding_guid=$(echo "$bindings_response" | jq -r \ + --arg app_guid "$app_guid" \ + '.resources[] | select(.relationships.app.data.guid == $app_guid) | .guid' | head -1) + if [ -z "$binding_guid" ]; then + echo "โŒ Error: Unable to retrieve SDM binding GUID for bookshop-mt-srv"; exit 1; + fi + + binding_details=$(cf curl "/v3/service_credential_bindings/${binding_guid}/details") + + cmis_client_secret=$(echo "$binding_details" | jq -r '.credentials.uaa.clientsecret // .credentials.clientsecret // empty') + if [ -z "$cmis_client_secret" ] || [ "$cmis_client_secret" == "null" ]; then + echo "โŒ Error: SDM clientsecret is not set or is null"; exit 1; + fi + echo "::add-mask::$cmis_client_secret" + + cmis_client_id=$(echo "$binding_details" | jq -r '.credentials.uaa.clientid // .credentials.clientid // empty') + if [ -z "$cmis_client_id" ] || [ "$cmis_client_id" == "null" ]; then + echo "โŒ Error: SDM clientid is not set or is null"; exit 1; + fi + echo "::add-mask::$cmis_client_id" + + cmis_url=$(echo "$binding_details" | jq -r '.credentials.uri // .credentials.endpoints.ecm_service // .credentials.url // empty') + if [ -z "$cmis_url" ]; then + echo "โŒ Error: SDM CMIS URL not found in binding details"; exit 1; + fi + echo "::add-mask::$cmis_url" + + printf 'CMIS_CLIENT_SECRET=%s\n' "$cmis_client_secret" >> $GITHUB_OUTPUT + printf 'CMIS_CLIENT_ID=%s\n' "$cmis_client_id" >> $GITHUB_OUTPUT + printf 'CMIS_URL=%s\n' "$cmis_url" >> $GITHUB_OUTPUT + echo "โœ… SDM CMIS credentials fetched successfully!" + + - name: Prepare credentials file ๐Ÿ“ + env: + CLIENT_SECRET: ${{ steps.fetch_credentials.outputs.CLIENT_SECRET }} + CLIENT_ID: ${{ steps.fetch_credentials.outputs.CLIENT_ID }} + CLIENT_SECRET_MT: ${{ steps.fetch_credentials_mt.outputs.CLIENT_SECRET_MT }} + CLIENT_ID_MT: ${{ steps.fetch_credentials_mt.outputs.CLIENT_ID_MT }} + CMIS_CLIENT_ID: ${{ steps.fetch_credentials_cmis.outputs.CMIS_CLIENT_ID }} + CMIS_CLIENT_SECRET: ${{ steps.fetch_credentials_cmis.outputs.CMIS_CLIENT_SECRET }} + CMIS_URL_FROM_CF: ${{ steps.fetch_credentials_cmis.outputs.CMIS_URL }} + CF_ORG: ${{ secrets.CF_ORG }} + CAPAUTH_URL: ${{ secrets.CAPAUTH_URL }} + AUTHURLMT1: ${{ secrets.AUTHURLMT1 }} + AUTHURLMT2: ${{ secrets.AUTHURLMT2 }} + CF_USER: ${{ secrets.CF_USER }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD }} + NOSDMROLEUSERNAME: ${{ secrets.NOSDMROLEUSERNAME }} + NOSDMROLEUSERPASSWORD: ${{ secrets.NOSDMROLEUSERPASSWORD }} + DEFAULTREPOSITORYID: ${{ secrets.DEFAULTREPOSITORYID }} + DEFAULTREPOSITORYIDMT: ${{ secrets.DEFAULTREPOSITORYIDMT }} + CF_SPACE: ${{ secrets.CF_SPACE }} + run: | + echo "๐Ÿš€ Preparing credentials for ${{ matrix.tokenFlow }} - ${{ matrix.tenant }}..." + set +x + set -e + PROPERTIES_FILE="sdm/src/test/resources/credentials.properties" + appUrl="$CF_ORG-$CF_SPACE-demoappjava-srv.cfapps.eu12.hana.ondemand.com" + appUrlMT="$CF_ORG-$CF_SPACE-bookshop-mt-srv.cfapps.eu12.hana.ondemand.com" + authUrl="$CAPAUTH_URL" + authUrlMT1="$AUTHURLMT1" + authUrlMT2="$AUTHURLMT2" + clientID="$CLIENT_ID" + clientSecret="$CLIENT_SECRET" + clientIDMT="$CLIENT_ID_MT" + clientSecretMT="$CLIENT_SECRET_MT" + username="$CF_USER" + password="$CF_PASSWORD" + noSDMRoleUsername="$NOSDMROLEUSERNAME" + noSDMRoleUserPassword="$NOSDMROLEUSERPASSWORD" + CMIS_URL="${{ env.CMIS_URL_FROM_CF }}" + cmisClientID="$CMIS_CLIENT_ID" + cmisClientSecret="$CMIS_CLIENT_SECRET" + defaultRepositoryID="$DEFAULTREPOSITORYID" + defaultRepositoryIDMT="$DEFAULTREPOSITORYIDMT" + + echo "::add-mask::$clientSecret" + echo "::add-mask::$clientID" + echo "::add-mask::$clientSecretMT" + echo "::add-mask::$clientIDMT" + echo "::add-mask::$username" + echo "::add-mask::$password" + echo "::add-mask::$noSDMRoleUsername" + echo "::add-mask::$noSDMRoleUserPassword" + echo "::add-mask::$CMIS_URL" + echo "::add-mask::$cmisClientID" + echo "::add-mask::$cmisClientSecret" + + if [ -z "$appUrl" ]; then echo "โŒ Error: appUrl is not set"; exit 1; fi + if [ -z "$appUrlMT" ]; then echo "โŒ Error: appUrlMT is not set"; exit 1; fi + if [ -z "$authUrl" ]; then echo "โŒ Error: authUrl is not set"; exit 1; fi + if [ -z "$authUrlMT1" ]; then echo "โŒ Error: authUrlMT1 is not set"; exit 1; fi + if [ -z "$authUrlMT2" ]; then echo "โŒ Error: authUrlMT2 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 "$clientIDMT" ]; then echo "โŒ Error: clientIDMT is not set"; exit 1; fi + if [ -z "$clientSecretMT" ]; then echo "โŒ Error: clientSecretMT 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 + if [ -z "$noSDMRoleUsername" ]; then echo "โŒ Error: noSDMRoleUsername is not set"; exit 1; fi + if [ -z "$noSDMRoleUserPassword" ]; then echo "โŒ Error: noSDMRoleUserPassword is not set"; exit 1; fi + if [ -z "$CMIS_URL" ]; then echo "โŒ Error: CMIS_URL is not set"; exit 1; fi + if [ -z "$cmisClientID" ]; then echo "โŒ Error: cmisClientID is not set"; exit 1; fi + if [ -z "$cmisClientSecret" ]; then echo "โŒ Error: cmisClientSecret is not set"; exit 1; fi + + cat > "$PROPERTIES_FILE" < /dev/null; then + sudo apt-get update && sudo apt-get install -y jq + fi + + + - name: Login to Cloud Foundry ๐Ÿ”‘ + env: + CF_API: ${{ secrets.CF_API }} + CF_USER: ${{ secrets.CF_USER }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD }} + CF_ORG: ${{ secrets.CF_ORG }} + CF_SPACE: ${{ secrets.CF_SPACE }} + run: | + set +x + echo "::add-mask::$CF_API" + echo "::add-mask::$CF_USER" + echo "::add-mask::$CF_PASSWORD" + echo "::add-mask::$CF_ORG" + echo "::add-mask::$CF_SPACE" + echo "๐Ÿ”„ Logging in to Cloud Foundry using space: $CF_SPACE" + cf login -a "$CF_API" \ + -u "$CF_USER" \ + -p "$CF_PASSWORD" \ + -o "$CF_ORG" \ + -s $CF_SPACE > /dev/null + + - name: Fetch and Escape Client Details for single tenant ๐Ÿ” + id: fetch_credentials + run: | + echo "Fetching client details for single tenant..." + 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 + + 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 + + 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 + escapedClientSecret=$(echo "$clientSecret" | sed 's/\$/\\$/g') + echo "::add-mask::$escapedClientSecret" + + clientID=$(echo "$binding_details" | jq -r '.credentials.clientid') + if [ -z "$clientID" ] || [ "$clientID" == "null" ]; then + echo "โŒ Error: clientID is not set or is null"; exit 1; + fi + echo "::add-mask::$clientID" + + echo "CLIENT_SECRET=$escapedClientSecret" >> $GITHUB_OUTPUT + echo "CLIENT_ID=$clientID" >> $GITHUB_OUTPUT + echo "โœ… Client details fetched successfully!" + + - name: Fetch and Escape Client Details for multi tenant ๐Ÿ” + id: fetch_credentials_mt + run: | + echo "Fetching client details for multi tenant..." + service_instance_guid=$(cf service bookshop-mt-uaa --guid) + if [ -z "$service_instance_guid" ]; then + echo "โŒ Error: Unable to retrieve service instance GUID"; exit 1; + fi + + 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 + + binding_details=$(cf curl "/v3/service_credential_bindings/${binding_guid}/details") + + clientSecret_mt=$(echo "$binding_details" | jq -r '.credentials.clientsecret') + if [ -z "$clientSecret_mt" ] || [ "$clientSecret_mt" == "null" ]; then + echo "โŒ Error: clientSecret_mt is not set or is null"; exit 1; + fi + escapedClientSecret_mt=$(echo "$clientSecret_mt" | sed 's/\$/\\$/g') + echo "::add-mask::$escapedClientSecret_mt" + + clientID_mt=$(echo "$binding_details" | jq -r '.credentials.clientid') + if [ -z "$clientID_mt" ] || [ "$clientID_mt" == "null" ]; then + echo "โŒ Error: clientID_mt is not set or is null"; exit 1; + fi + echo "::add-mask::$clientID_mt" + + echo "CLIENT_SECRET_MT=$escapedClientSecret_mt" >> $GITHUB_OUTPUT + echo "CLIENT_ID_MT=$clientID_mt" >> $GITHUB_OUTPUT + echo "โœ… Multi-tenant client details fetched successfully!" + + - name: Fetch and Escape SDM CMIS Credentials ๐Ÿ” + id: fetch_credentials_cmis + run: | + echo "๐Ÿ”„ Fetching SDM CMIS credentials from CF service binding..." + service_instance_guid=$(cf service sdm --guid) + if [ -z "$service_instance_guid" ]; then + echo "โŒ Error: Unable to retrieve SDM service instance GUID"; exit 1; + fi + + bindings_response=$(cf curl "/v3/service_credential_bindings?service_instance_guids=${service_instance_guid}") + app_guid=$(cf app bookshop-mt-srv --guid) + binding_guid=$(echo "$bindings_response" | jq -r \ + --arg app_guid "$app_guid" \ + '.resources[] | select(.relationships.app.data.guid == $app_guid) | .guid' | head -1) + if [ -z "$binding_guid" ]; then + echo "โŒ Error: Unable to retrieve SDM binding GUID for bookshop-mt-srv"; exit 1; + fi + + binding_details=$(cf curl "/v3/service_credential_bindings/${binding_guid}/details") + + cmis_client_secret=$(echo "$binding_details" | jq -r '.credentials.uaa.clientsecret // .credentials.clientsecret // empty') + if [ -z "$cmis_client_secret" ] || [ "$cmis_client_secret" == "null" ]; then + echo "โŒ Error: SDM clientsecret is not set or is null"; exit 1; + fi + echo "::add-mask::$cmis_client_secret" + + cmis_client_id=$(echo "$binding_details" | jq -r '.credentials.uaa.clientid // .credentials.clientid // empty') + if [ -z "$cmis_client_id" ] || [ "$cmis_client_id" == "null" ]; then + echo "โŒ Error: SDM clientid is not set or is null"; exit 1; + fi + echo "::add-mask::$cmis_client_id" + + cmis_url=$(echo "$binding_details" | jq -r '.credentials.uri // .credentials.endpoints.ecm_service // .credentials.url // empty') + if [ -z "$cmis_url" ]; then + echo "โŒ Error: SDM CMIS URL not found in binding details"; exit 1; + fi + echo "::add-mask::$cmis_url" + + printf 'CMIS_CLIENT_SECRET=%s\n' "$cmis_client_secret" >> $GITHUB_OUTPUT + printf 'CMIS_CLIENT_ID=%s\n' "$cmis_client_id" >> $GITHUB_OUTPUT + printf 'CMIS_URL=%s\n' "$cmis_url" >> $GITHUB_OUTPUT + echo "โœ… SDM CMIS credentials fetched successfully!" + + - name: Prepare credentials file ๐Ÿ“ + env: + CLIENT_SECRET: ${{ steps.fetch_credentials.outputs.CLIENT_SECRET }} + CLIENT_ID: ${{ steps.fetch_credentials.outputs.CLIENT_ID }} + CLIENT_SECRET_MT: ${{ steps.fetch_credentials_mt.outputs.CLIENT_SECRET_MT }} + CLIENT_ID_MT: ${{ steps.fetch_credentials_mt.outputs.CLIENT_ID_MT }} + CMIS_CLIENT_ID: ${{ steps.fetch_credentials_cmis.outputs.CMIS_CLIENT_ID }} + CMIS_CLIENT_SECRET: ${{ steps.fetch_credentials_cmis.outputs.CMIS_CLIENT_SECRET }} + CMIS_URL_FROM_CF: ${{ steps.fetch_credentials_cmis.outputs.CMIS_URL }} + CF_ORG: ${{ secrets.CF_ORG }} + CAPAUTH_URL: ${{ secrets.CAPAUTH_URL }} + AUTHURLMT1: ${{ secrets.AUTHURLMT1 }} + AUTHURLMT2: ${{ secrets.AUTHURLMT2 }} + CF_USER: ${{ secrets.CF_USER }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD }} + NOSDMROLEUSERNAME: ${{ secrets.NOSDMROLEUSERNAME }} + NOSDMROLEUSERPASSWORD: ${{ secrets.NOSDMROLEUSERPASSWORD }} + DEFAULTREPOSITORYID: ${{ secrets.DEFAULTREPOSITORYID }} + DEFAULTREPOSITORYIDMT: ${{ secrets.DEFAULTREPOSITORYIDMT }} + CF_SPACE: ${{ secrets.CF_SPACE }} + run: | + echo "๐Ÿš€ Preparing credentials for ${{ matrix.tokenFlow }} - ${{ matrix.tenant }}..." + set +x + set -e + PROPERTIES_FILE="sdm/src/test/resources/credentials.properties" + appUrl="$CF_ORG-$CF_SPACE-demoappjava-srv.cfapps.eu12.hana.ondemand.com" + appUrlMT="$CF_ORG-$CF_SPACE-bookshop-mt-srv.cfapps.eu12.hana.ondemand.com" + authUrl="$CAPAUTH_URL" + authUrlMT1="$AUTHURLMT1" + authUrlMT2="$AUTHURLMT2" + clientID="$CLIENT_ID" + clientSecret="$CLIENT_SECRET" + clientIDMT="$CLIENT_ID_MT" + clientSecretMT="$CLIENT_SECRET_MT" + username="$CF_USER" + password="$CF_PASSWORD" + noSDMRoleUsername="$NOSDMROLEUSERNAME" + noSDMRoleUserPassword="$NOSDMROLEUSERPASSWORD" + CMIS_URL="${{ env.CMIS_URL_FROM_CF }}" + cmisClientID="$CMIS_CLIENT_ID" + cmisClientSecret="$CMIS_CLIENT_SECRET" + defaultRepositoryID="$DEFAULTREPOSITORYID" + defaultRepositoryIDMT="$DEFAULTREPOSITORYIDMT" + + echo "::add-mask::$clientSecret" + echo "::add-mask::$clientID" + echo "::add-mask::$clientSecretMT" + echo "::add-mask::$clientIDMT" + echo "::add-mask::$username" + echo "::add-mask::$password" + echo "::add-mask::$noSDMRoleUsername" + echo "::add-mask::$noSDMRoleUserPassword" + echo "::add-mask::$CMIS_URL" + echo "::add-mask::$cmisClientID" + echo "::add-mask::$cmisClientSecret" + + if [ -z "$appUrl" ]; then echo "โŒ Error: appUrl is not set"; exit 1; fi + if [ -z "$appUrlMT" ]; then echo "โŒ Error: appUrlMT is not set"; exit 1; fi + if [ -z "$authUrl" ]; then echo "โŒ Error: authUrl is not set"; exit 1; fi + if [ -z "$authUrlMT1" ]; then echo "โŒ Error: authUrlMT1 is not set"; exit 1; fi + if [ -z "$authUrlMT2" ]; then echo "โŒ Error: authUrlMT2 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 "$clientIDMT" ]; then echo "โŒ Error: clientIDMT is not set"; exit 1; fi + if [ -z "$clientSecretMT" ]; then echo "โŒ Error: clientSecretMT 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 + if [ -z "$noSDMRoleUsername" ]; then echo "โŒ Error: noSDMRoleUsername is not set"; exit 1; fi + if [ -z "$noSDMRoleUserPassword" ]; then echo "โŒ Error: noSDMRoleUserPassword is not set"; exit 1; fi + if [ -z "$CMIS_URL" ]; then echo "โŒ Error: CMIS_URL is not set"; exit 1; fi + if [ -z "$cmisClientID" ]; then echo "โŒ Error: cmisClientID is not set"; exit 1; fi + if [ -z "$cmisClientSecret" ]; then echo "โŒ Error: cmisClientSecret is not set"; exit 1; fi + + cat > "$PROPERTIES_FILE" < /dev/null; then + sudo apt-get update && sudo apt-get install -y jq + fi + + - name: Login to Cloud Foundry ๐Ÿ”‘ + env: + CF_API: ${{ secrets.CF_API }} + CF_USER: ${{ secrets.CF_USER }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD }} + CF_ORG: ${{ secrets.CF_ORG }} + CF_SPACE: ${{ secrets.CF_SPACE }} + run: | + set +x + echo "::add-mask::$CF_API" + echo "::add-mask::$CF_USER" + echo "::add-mask::$CF_PASSWORD" + echo "::add-mask::$CF_ORG" + echo "::add-mask::$CF_SPACE" + echo "๐Ÿ”„ Logging in to Cloud Foundry using space: $CF_SPACE" + cf login -a "$CF_API" \ + -u "$CF_USER" \ + -p "$CF_PASSWORD" \ + -o "$CF_ORG" \ + -s $CF_SPACE > /dev/null + + - name: Fetch and Escape Client Details for single tenant ๐Ÿ” + id: fetch_credentials + run: | + echo "๐Ÿ”„ Fetching client details for single tenant..." + 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 + 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 + 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 + escapedClientSecret=$(echo "$clientSecret" | sed 's/\$/\\$/g') + echo "::add-mask::$escapedClientSecret" + clientID=$(echo "$binding_details" | jq -r '.credentials.clientid') + if [ -z "$clientID" ] || [ "$clientID" == "null" ]; then + echo "โŒ Error: clientID is not set or is null"; exit 1; + fi + echo "::add-mask::$clientID" + echo "CLIENT_SECRET=$escapedClientSecret" >> $GITHUB_OUTPUT + echo "CLIENT_ID=$clientID" >> $GITHUB_OUTPUT + echo "โœ… Client details fetched successfully!" + + - name: Fetch and Escape SDM CMIS Credentials ๐Ÿ” + id: fetch_credentials_cmis + run: | + echo "๐Ÿ”„ Fetching SDM CMIS credentials from CF service binding..." + service_instance_guid=$(cf service sdm --guid) + if [ -z "$service_instance_guid" ]; then + echo "โŒ Error: Unable to retrieve SDM service instance GUID"; exit 1; + fi + bindings_response=$(cf curl "/v3/service_credential_bindings?service_instance_guids=${service_instance_guid}") + app_guid=$(cf app demoappjava-srv --guid) + binding_guid=$(echo "$bindings_response" | jq -r \ + --arg app_guid "$app_guid" \ + '.resources[] | select(.relationships.app.data.guid == $app_guid) | .guid' | head -1) + if [ -z "$binding_guid" ]; then + echo "โŒ Error: Unable to retrieve SDM binding GUID for demoappjava-srv"; exit 1; + fi + binding_details=$(cf curl "/v3/service_credential_bindings/${binding_guid}/details") + cmis_client_secret=$(echo "$binding_details" | jq -r '.credentials.uaa.clientsecret // .credentials.clientsecret // empty') + if [ -z "$cmis_client_secret" ] || [ "$cmis_client_secret" == "null" ]; then + echo "โŒ Error: SDM clientsecret is not set or is null"; exit 1; + fi + echo "::add-mask::$cmis_client_secret" + cmis_client_id=$(echo "$binding_details" | jq -r '.credentials.uaa.clientid // .credentials.clientid // empty') + if [ -z "$cmis_client_id" ] || [ "$cmis_client_id" == "null" ]; then + echo "โŒ Error: SDM clientid is not set or is null"; exit 1; + fi + echo "::add-mask::$cmis_client_id" + cmis_url=$(echo "$binding_details" | jq -r '.credentials.uri // .credentials.endpoints.ecm_service // .credentials.url // empty') + if [ -z "$cmis_url" ]; then + echo "โŒ Error: SDM CMIS URL not found in binding details"; exit 1; + fi + echo "::add-mask::$cmis_url" + printf 'CMIS_CLIENT_SECRET=%s\n' "$cmis_client_secret" >> $GITHUB_OUTPUT + printf 'CMIS_CLIENT_ID=%s\n' "$cmis_client_id" >> $GITHUB_OUTPUT + printf 'CMIS_URL=%s\n' "$cmis_url" >> $GITHUB_OUTPUT + echo "โœ… SDM CMIS credentials fetched successfully!" + + - name: Run active-entity integration tests ๐ŸŽฏ (${{ matrix.tokenFlow }} - ${{ matrix.testClass }}) + env: + CLIENT_SECRET: ${{ steps.fetch_credentials.outputs.CLIENT_SECRET }} + CLIENT_ID: ${{ steps.fetch_credentials.outputs.CLIENT_ID }} + CMIS_CLIENT_ID: ${{ steps.fetch_credentials_cmis.outputs.CMIS_CLIENT_ID }} + CMIS_CLIENT_SECRET: ${{ steps.fetch_credentials_cmis.outputs.CMIS_CLIENT_SECRET }} + CMIS_URL_FROM_CF: ${{ steps.fetch_credentials_cmis.outputs.CMIS_URL }} + CF_ORG: ${{ secrets.CF_ORG }} + CAPAUTH_URL: ${{ secrets.CAPAUTH_URL }} + CF_USER: ${{ secrets.CF_USER }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD }} + NOSDMROLEUSERNAME: ${{ secrets.NOSDMROLEUSERNAME }} + NOSDMROLEUSERPASSWORD: ${{ secrets.NOSDMROLEUSERPASSWORD }} + VERSIONEDREPOSITORYID: ${{ secrets.VERSIONEDREPOSITORYID }} + VIRUSSCANREPOSITORYID: ${{ secrets.VIRUSSCANREPOSITORYID }} + DEFAULTREPOSITORYID: ${{ secrets.DEFAULTREPOSITORYID }} + CF_SPACE: ${{ secrets.CF_SPACE }} + run: | + set +x + echo "๐Ÿš€ Starting active-entity integration tests for ${{ matrix.tokenFlow }} - ${{ matrix.testClass }}..." + set -e + PROPERTIES_FILE="sdm/src/test/resources/credentials.properties" + appUrl="$CF_ORG-$CF_SPACE-demoappjava-srv.cfapps.eu12.hana.ondemand.com" + authUrl="$CAPAUTH_URL" + clientID="$CLIENT_ID" + clientSecret="$CLIENT_SECRET" + username="$CF_USER" + password="$CF_PASSWORD" + noSDMRoleUsername="$NOSDMROLEUSERNAME" + noSDMRoleUserPassword="$NOSDMROLEUSERPASSWORD" + versionedRepositoryID="$VERSIONEDREPOSITORYID" + virusScanRepositoryID="$VIRUSSCANREPOSITORYID" + defaultRepositoryID="$DEFAULTREPOSITORYID" + CMIS_URL="$CMIS_URL_FROM_CF" + cmisClientID="$CMIS_CLIENT_ID" + cmisClientSecret="$CMIS_CLIENT_SECRET" + echo "::add-mask::$clientSecret" + echo "::add-mask::$clientID" + echo "::add-mask::$username" + echo "::add-mask::$password" + echo "::add-mask::$noSDMRoleUsername" + echo "::add-mask::$noSDMRoleUserPassword" + echo "::add-mask::$versionedRepositoryID" + echo "::add-mask::$virusScanRepositoryID" + echo "::add-mask::$defaultRepositoryID" + echo "::add-mask::$CMIS_URL" + echo "::add-mask::$cmisClientID" + echo "::add-mask::$cmisClientSecret" + 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 + if [ -z "$noSDMRoleUsername" ]; then echo "โŒ Error: noSDMRoleUsername is not set"; exit 1; fi + if [ -z "$noSDMRoleUserPassword" ]; then echo "โŒ Error: noSDMRoleUserPassword is not set"; exit 1; fi + if [ -z "$versionedRepositoryID" ]; then echo "โŒ Error: versionedRepositoryID is not set"; exit 1; fi + if [ -z "$virusScanRepositoryID" ]; then echo "โŒ Error: virusScanRepositoryID is not set"; exit 1; fi + if [ -z "$defaultRepositoryID" ]; then echo "โŒ Error: defaultRepositoryID is not set"; exit 1; fi + if [ -z "$CMIS_URL" ]; then echo "โŒ Error: CMIS_URL is not set"; exit 1; fi + if [ -z "$cmisClientID" ]; then echo "โŒ Error: cmisClientID is not set"; exit 1; fi + if [ -z "$cmisClientSecret" ]; then echo "โŒ Error: cmisClientSecret is not set"; exit 1; fi + cat > "$PROPERTIES_FILE" < /dev/null; then + sudo apt-get update && sudo apt-get install -y jq + fi + + - name: Login to Cloud Foundry ๐Ÿ”‘ + env: + CF_API: ${{ secrets.CF_API }} + CF_USER: ${{ secrets.CF_USER }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD }} + CF_ORG: ${{ secrets.CF_ORG }} + CF_SPACE: ${{ secrets.CF_SPACE }} + run: | + set +x + echo "::add-mask::$CF_API" + echo "::add-mask::$CF_USER" + echo "::add-mask::$CF_PASSWORD" + echo "::add-mask::$CF_ORG" + echo "::add-mask::$CF_SPACE" + echo "๐Ÿ”„ Logging in to Cloud Foundry using space: $CF_SPACE" + cf login -a "$CF_API" \ + -u "$CF_USER" \ + -p "$CF_PASSWORD" \ + -o "$CF_ORG" \ + -s $CF_SPACE > /dev/null + + - name: Fetch and Escape Client Details for single tenant ๐Ÿ” + id: fetch_credentials + run: | + echo "๐Ÿ”„ Fetching client details for single tenant..." + 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 + 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 + 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 + escapedClientSecret=$(echo "$clientSecret" | sed 's/\$/\\$/g') + echo "::add-mask::$escapedClientSecret" + clientID=$(echo "$binding_details" | jq -r '.credentials.clientid') + if [ -z "$clientID" ] || [ "$clientID" == "null" ]; then + echo "โŒ Error: clientID is not set or is null"; exit 1; + fi + echo "::add-mask::$clientID" + echo "CLIENT_SECRET=$escapedClientSecret" >> $GITHUB_OUTPUT + echo "CLIENT_ID=$clientID" >> $GITHUB_OUTPUT + echo "โœ… Client details fetched successfully!" + + - name: Fetch and Escape SDM CMIS Credentials ๐Ÿ” + id: fetch_credentials_cmis + run: | + echo "๐Ÿ”„ Fetching SDM CMIS credentials from CF service binding..." + service_instance_guid=$(cf service sdm --guid) + if [ -z "$service_instance_guid" ]; then + echo "โŒ Error: Unable to retrieve SDM service instance GUID"; exit 1; + fi + bindings_response=$(cf curl "/v3/service_credential_bindings?service_instance_guids=${service_instance_guid}") + app_guid=$(cf app demoappjava-srv --guid) + binding_guid=$(echo "$bindings_response" | jq -r \ + --arg app_guid "$app_guid" \ + '.resources[] | select(.relationships.app.data.guid == $app_guid) | .guid' | head -1) + if [ -z "$binding_guid" ]; then + echo "โŒ Error: Unable to retrieve SDM binding GUID for demoappjava-srv"; exit 1; + fi + binding_details=$(cf curl "/v3/service_credential_bindings/${binding_guid}/details") + cmis_client_secret=$(echo "$binding_details" | jq -r '.credentials.uaa.clientsecret // .credentials.clientsecret // empty') + if [ -z "$cmis_client_secret" ] || [ "$cmis_client_secret" == "null" ]; then + echo "โŒ Error: SDM clientsecret is not set or is null"; exit 1; + fi + echo "::add-mask::$cmis_client_secret" + cmis_client_id=$(echo "$binding_details" | jq -r '.credentials.uaa.clientid // .credentials.clientid // empty') + if [ -z "$cmis_client_id" ] || [ "$cmis_client_id" == "null" ]; then + echo "โŒ Error: SDM clientid is not set or is null"; exit 1; + fi + echo "::add-mask::$cmis_client_id" + cmis_url=$(echo "$binding_details" | jq -r '.credentials.uri // .credentials.endpoints.ecm_service // .credentials.url // empty') + if [ -z "$cmis_url" ]; then + echo "โŒ Error: SDM CMIS URL not found in binding details"; exit 1; + fi + echo "::add-mask::$cmis_url" + printf 'CMIS_CLIENT_SECRET=%s\n' "$cmis_client_secret" >> $GITHUB_OUTPUT + printf 'CMIS_CLIENT_ID=%s\n' "$cmis_client_id" >> $GITHUB_OUTPUT + printf 'CMIS_URL=%s\n' "$cmis_url" >> $GITHUB_OUTPUT + echo "โœ… SDM CMIS credentials fetched successfully!" + + - name: Run active-entity integration tests ๐ŸŽฏ (${{ matrix.tokenFlow }} - ${{ matrix.testClass }}) + env: + CLIENT_SECRET: ${{ steps.fetch_credentials.outputs.CLIENT_SECRET }} + CLIENT_ID: ${{ steps.fetch_credentials.outputs.CLIENT_ID }} + CMIS_CLIENT_ID: ${{ steps.fetch_credentials_cmis.outputs.CMIS_CLIENT_ID }} + CMIS_CLIENT_SECRET: ${{ steps.fetch_credentials_cmis.outputs.CMIS_CLIENT_SECRET }} + CMIS_URL_FROM_CF: ${{ steps.fetch_credentials_cmis.outputs.CMIS_URL }} + CF_ORG: ${{ secrets.CF_ORG }} + CAPAUTH_URL: ${{ secrets.CAPAUTH_URL }} + CF_USER: ${{ secrets.CF_USER }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD }} + NOSDMROLEUSERNAME: ${{ secrets.NOSDMROLEUSERNAME }} + NOSDMROLEUSERPASSWORD: ${{ secrets.NOSDMROLEUSERPASSWORD }} + VERSIONEDREPOSITORYID: ${{ secrets.VERSIONEDREPOSITORYID }} + VIRUSSCANREPOSITORYID: ${{ secrets.VIRUSSCANREPOSITORYID }} + DEFAULTREPOSITORYID: ${{ secrets.DEFAULTREPOSITORYID }} + CF_SPACE: ${{ secrets.CF_SPACE }} + run: | + set +x + echo "๐Ÿš€ Starting active-entity integration tests for ${{ matrix.tokenFlow }} - ${{ matrix.testClass }}..." + set -e + PROPERTIES_FILE="sdm/src/test/resources/credentials.properties" + appUrl="$CF_ORG-$CF_SPACE-demoappjava-srv.cfapps.eu12.hana.ondemand.com" + authUrl="$CAPAUTH_URL" + clientID="$CLIENT_ID" + clientSecret="$CLIENT_SECRET" + username="$CF_USER" + password="$CF_PASSWORD" + noSDMRoleUsername="$NOSDMROLEUSERNAME" + noSDMRoleUserPassword="$NOSDMROLEUSERPASSWORD" + versionedRepositoryID="$VERSIONEDREPOSITORYID" + virusScanRepositoryID="$VIRUSSCANREPOSITORYID" + defaultRepositoryID="$DEFAULTREPOSITORYID" + CMIS_URL="$CMIS_URL_FROM_CF" + cmisClientID="$CMIS_CLIENT_ID" + cmisClientSecret="$CMIS_CLIENT_SECRET" + echo "::add-mask::$clientSecret" + echo "::add-mask::$clientID" + echo "::add-mask::$username" + echo "::add-mask::$password" + echo "::add-mask::$noSDMRoleUsername" + echo "::add-mask::$noSDMRoleUserPassword" + echo "::add-mask::$versionedRepositoryID" + echo "::add-mask::$virusScanRepositoryID" + echo "::add-mask::$defaultRepositoryID" + echo "::add-mask::$CMIS_URL" + echo "::add-mask::$cmisClientID" + echo "::add-mask::$cmisClientSecret" + 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 + if [ -z "$noSDMRoleUsername" ]; then echo "โŒ Error: noSDMRoleUsername is not set"; exit 1; fi + if [ -z "$noSDMRoleUserPassword" ]; then echo "โŒ Error: noSDMRoleUserPassword is not set"; exit 1; fi + if [ -z "$versionedRepositoryID" ]; then echo "โŒ Error: versionedRepositoryID is not set"; exit 1; fi + if [ -z "$virusScanRepositoryID" ]; then echo "โŒ Error: virusScanRepositoryID is not set"; exit 1; fi + if [ -z "$defaultRepositoryID" ]; then echo "โŒ Error: defaultRepositoryID is not set"; exit 1; fi + if [ -z "$CMIS_URL" ]; then echo "โŒ Error: CMIS_URL is not set"; exit 1; fi + if [ -z "$cmisClientID" ]; then echo "โŒ Error: cmisClientID is not set"; exit 1; fi + if [ -z "$cmisClientSecret" ]; then echo "โŒ Error: cmisClientSecret is not set"; exit 1; fi + cat > "$PROPERTIES_FILE" < /dev/null; then + sudo apt-get update && sudo apt-get install -y jq + fi + + - name: Determine Cloud Foundry Space ๐ŸŒŒ + id: determine_space + env: + CF_SPACE: ${{ secrets.CF_SPACE }} + run: | + if [ "${{ github.event.inputs.cf_space }}" == "developcap" ]; then + space="$CF_SPACE" + else + space="${{ github.event.inputs.cf_space }}" + fi + echo "๐ŸŒ Space determined: $space" + echo "space=$space" >> $GITHUB_OUTPUT + + - name: Login to Cloud Foundry ๐Ÿ”‘ + env: + CF_API: ${{ secrets.CF_API }} + CF_USER: ${{ secrets.CF_USER }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD }} + CF_ORG: ${{ secrets.CF_ORG }} + CF_SPACE: ${{ secrets.CF_SPACE }} + run: | + set +x + echo "::add-mask::$CF_API" + echo "::add-mask::$CF_USER" + echo "::add-mask::$CF_PASSWORD" + echo "::add-mask::$CF_ORG" + echo "::add-mask::$CF_SPACE" + echo "๐Ÿ”„ Logging in to Cloud Foundry using space: ${{ steps.determine_space.outputs.space }}" + cf login -a "$CF_API" \ + -u "$CF_USER" \ + -p "$CF_PASSWORD" \ + -o "$CF_ORG" \ + -s ${{ steps.determine_space.outputs.space }} > /dev/null + + - name: Fetch and Escape Client Details for single tenant ๐Ÿ” + id: fetch_credentials + run: | + echo "๐Ÿ”„ Fetching client details for single tenant..." + 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 + 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 + 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 + escapedClientSecret=$(echo "$clientSecret" | sed 's/\$/\\$/g') + echo "::add-mask::$escapedClientSecret" + clientID=$(echo "$binding_details" | jq -r '.credentials.clientid') + if [ -z "$clientID" ] || [ "$clientID" == "null" ]; then + echo "โŒ Error: clientID is not set or is null"; exit 1; + fi + echo "::add-mask::$clientID" + echo "CLIENT_SECRET=$escapedClientSecret" >> $GITHUB_OUTPUT + echo "CLIENT_ID=$clientID" >> $GITHUB_OUTPUT + echo "โœ… Client details fetched successfully!" + + - name: Fetch and Escape SDM CMIS Credentials ๐Ÿ” + id: fetch_credentials_cmis + run: | + echo "๐Ÿ”„ Fetching SDM CMIS credentials from CF service binding..." + service_instance_guid=$(cf service sdm --guid) + if [ -z "$service_instance_guid" ]; then + echo "โŒ Error: Unable to retrieve SDM service instance GUID"; exit 1; + fi + bindings_response=$(cf curl "/v3/service_credential_bindings?service_instance_guids=${service_instance_guid}") + app_guid=$(cf app demoappjava-srv --guid) + binding_guid=$(echo "$bindings_response" | jq -r \ + --arg app_guid "$app_guid" \ + '.resources[] | select(.relationships.app.data.guid == $app_guid) | .guid' | head -1) + if [ -z "$binding_guid" ]; then + echo "โŒ Error: Unable to retrieve SDM binding GUID for demoappjava-srv"; exit 1; + fi + binding_details=$(cf curl "/v3/service_credential_bindings/${binding_guid}/details") + cmis_client_secret=$(echo "$binding_details" | jq -r '.credentials.uaa.clientsecret // .credentials.clientsecret // empty') + if [ -z "$cmis_client_secret" ] || [ "$cmis_client_secret" == "null" ]; then + echo "โŒ Error: SDM clientsecret is not set or is null"; exit 1; + fi + echo "::add-mask::$cmis_client_secret" + cmis_client_id=$(echo "$binding_details" | jq -r '.credentials.uaa.clientid // .credentials.clientid // empty') + if [ -z "$cmis_client_id" ] || [ "$cmis_client_id" == "null" ]; then + echo "โŒ Error: SDM clientid is not set or is null"; exit 1; + fi + echo "::add-mask::$cmis_client_id" + cmis_url=$(echo "$binding_details" | jq -r '.credentials.uri // .credentials.endpoints.ecm_service // .credentials.url // empty') + if [ -z "$cmis_url" ]; then + echo "โŒ Error: SDM CMIS URL not found in binding details"; exit 1; + fi + echo "::add-mask::$cmis_url" + printf 'CMIS_CLIENT_SECRET=%s\n' "$cmis_client_secret" >> $GITHUB_OUTPUT + printf 'CMIS_CLIENT_ID=%s\n' "$cmis_client_id" >> $GITHUB_OUTPUT + printf 'CMIS_URL=%s\n' "$cmis_url" >> $GITHUB_OUTPUT + echo "โœ… SDM CMIS credentials fetched successfully!" + + - name: Run active-entity integration tests ๐ŸŽฏ (${{ matrix.tokenFlow }} - ${{ matrix.testClass }}) + env: + CLIENT_SECRET: ${{ steps.fetch_credentials.outputs.CLIENT_SECRET }} + CLIENT_ID: ${{ steps.fetch_credentials.outputs.CLIENT_ID }} + CMIS_CLIENT_ID: ${{ steps.fetch_credentials_cmis.outputs.CMIS_CLIENT_ID }} + CMIS_CLIENT_SECRET: ${{ steps.fetch_credentials_cmis.outputs.CMIS_CLIENT_SECRET }} + CMIS_URL_FROM_CF: ${{ steps.fetch_credentials_cmis.outputs.CMIS_URL }} + CF_ORG: ${{ secrets.CF_ORG }} + CAPAUTH_URL: ${{ secrets.CAPAUTH_URL }} + CF_USER: ${{ secrets.CF_USER }} + CF_PASSWORD: ${{ secrets.CF_PASSWORD }} + NOSDMROLEUSERNAME: ${{ secrets.NOSDMROLEUSERNAME }} + NOSDMROLEUSERPASSWORD: ${{ secrets.NOSDMROLEUSERPASSWORD }} + VERSIONEDREPOSITORYID: ${{ secrets.VERSIONEDREPOSITORYID }} + VIRUSSCANREPOSITORYID: ${{ secrets.VIRUSSCANREPOSITORYID }} + DEFAULTREPOSITORYID: ${{ secrets.DEFAULTREPOSITORYID }} + run: | + set +x + echo "๐Ÿš€ Starting active-entity integration tests for ${{ matrix.tokenFlow }} - ${{ matrix.testClass }}..." + set -e + PROPERTIES_FILE="sdm/src/test/resources/credentials.properties" + appUrl="$CF_ORG-${{ steps.determine_space.outputs.space }}-demoappjava-srv.cfapps.eu12.hana.ondemand.com" + authUrl="$CAPAUTH_URL" + clientID="$CLIENT_ID" + clientSecret="$CLIENT_SECRET" + username="$CF_USER" + password="$CF_PASSWORD" + noSDMRoleUsername="$NOSDMROLEUSERNAME" + noSDMRoleUserPassword="$NOSDMROLEUSERPASSWORD" + versionedRepositoryID="$VERSIONEDREPOSITORYID" + virusScanRepositoryID="$VIRUSSCANREPOSITORYID" + defaultRepositoryID="$DEFAULTREPOSITORYID" + CMIS_URL="$CMIS_URL_FROM_CF" + cmisClientID="$CMIS_CLIENT_ID" + cmisClientSecret="$CMIS_CLIENT_SECRET" + echo "::add-mask::$clientSecret" + echo "::add-mask::$clientID" + echo "::add-mask::$username" + echo "::add-mask::$password" + echo "::add-mask::$noSDMRoleUsername" + echo "::add-mask::$noSDMRoleUserPassword" + echo "::add-mask::$versionedRepositoryID" + echo "::add-mask::$virusScanRepositoryID" + echo "::add-mask::$defaultRepositoryID" + echo "::add-mask::$CMIS_URL" + echo "::add-mask::$cmisClientID" + echo "::add-mask::$cmisClientSecret" + 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 + if [ -z "$noSDMRoleUsername" ]; then echo "โŒ Error: noSDMRoleUsername is not set"; exit 1; fi + if [ -z "$noSDMRoleUserPassword" ]; then echo "โŒ Error: noSDMRoleUserPassword is not set"; exit 1; fi + if [ -z "$versionedRepositoryID" ]; then echo "โŒ Error: versionedRepositoryID is not set"; exit 1; fi + if [ -z "$virusScanRepositoryID" ]; then echo "โŒ Error: virusScanRepositoryID is not set"; exit 1; fi + if [ -z "$defaultRepositoryID" ]; then echo "โŒ Error: defaultRepositoryID is not set"; exit 1; fi + if [ -z "$CMIS_URL" ]; then echo "โŒ Error: CMIS_URL is not set"; exit 1; fi + if [ -z "$cmisClientID" ]; then echo "โŒ Error: cmisClientID is not set"; exit 1; fi + if [ -z "$cmisClientSecret" ]; then echo "โŒ Error: cmisClientSecret is not set"; exit 1; fi + cat > "$PROPERTIES_FILE" < 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 entityName, String srvpath, String entityID) { MediaType mediaType = MediaType.parse("application/json"); Request request = @@ -1295,4 +1353,51 @@ public Map fetchChangelog( throw new IOException("Error fetching changelog: " + e.getMessage(), e); } } + + @Override + public String createAttachmentInActive( + String appUrl, String entityName, String facetName, String entityID) throws IOException { + String url = + "https://" + + appUrl + + "/odata/v4/" + + serviceName + + "/" + + entityName + + "(ID=" + + entityID + + ",IsActiveEntity=true)/" + + facetName + + "/" + + serviceName + + ".createAttachmentInActive"; + + RequestBody body = RequestBody.create("{}", MediaType.parse("application/json")); + + Request request = + new Request.Builder() + .url(url) + .post(body) + .addHeader("Authorization", token) + .addHeader("Content-Type", "application/json") + .build(); + + try (Response response = executeWithRetry(request)) { + if (!response.isSuccessful()) { + String errorBody = response.body() != null ? response.body().string() : ""; + System.out.println( + "createAttachmentInActive failed for " + + entityName + + "." + + facetName + + ". Error: " + + errorBody); + return errorBody; + } + return "Attachment created in active entity"; + } catch (IOException e) { + System.out.println("createAttachmentInActive failed: " + e.getMessage()); + return "Could not create attachment in active entity"; + } + } } diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/ApiInterface.java b/sdm/src/test/java/integration/com/sap/cds/sdm/ApiInterface.java index 034841d8..0cbe54f1 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/ApiInterface.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/ApiInterface.java @@ -13,6 +13,23 @@ public String createEntityDraft( public String createEntityDraft( String appUrl, String entityName, String entityName2, String srvpath, String bookID); + /** + * Same as {@link #createEntityDraft(String, String, String, String)} but with a caller-supplied + * author name so parallel matrix jobs can use distinct fixture markers and their cleanups won't + * clobber each other. + */ + public String createEntityDraftWithAuthor( + String appUrl, String entityName, String entityName2, String srvpath, String authorName); + + /** Chapter-in-book variant of {@link #createEntityDraftWithAuthor}. */ + public String createEntityDraftWithAuthor( + String appUrl, + String entityName, + String entityName2, + String srvpath, + String bookID, + String authorName); + public String editEntityDraft(String appUrl, String entityName, String srvpath, String entityID); public String saveEntityDraft(String appUrl, String entityName, String srvpath, String entityID); @@ -129,4 +146,7 @@ public String downloadSelectedAttachments( public String downloadSelectedAttachmentsDraft( String appUrl, String entityName, String facetName, String entityID, List ids) throws IOException; + + public String createAttachmentInActive( + String appUrl, String entityName, String facetName, String entityID) throws IOException; } diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java b/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java index f5e4125c..e0805f29 100644 --- a/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/ApiMT.java @@ -140,6 +140,64 @@ public String createEntityDraft( return ("Could not create entity"); } + @Override + public String createEntityDraftWithAuthor( + String appUrl, String entityName, String entityName2, String srvpath, String authorName) { + return createEntityDraftWithAuthor(appUrl, entityName, entityName2, srvpath, null, authorName); + } + + @Override + public String createEntityDraftWithAuthor( + String appUrl, + String entityName, + String entityName2, + String srvpath, + String bookID, + String authorName) { + MediaType mediaType = MediaType.parse("application/json"); + + String jsonBody; + if (bookID != null && !bookID.isEmpty()) { + // Creating a Chapter within a Book โ€” same as the default path + jsonBody = + "{\n \"title\": \"IntegrationTestEntity\",\n \"book_ID\": \"" + bookID + "\"\n}"; + } else { + // Creating a Book with a caller-supplied author name so parallel matrix jobs + // can use distinct fixture markers and their cleanups won't clobber each other. + jsonBody = + "{\n \"title\": \"IntegrationTestEntity\",\n \"" + + entityName2 + + "\": {\n \"ID\": \"41cf82fb-94bf-4d62-9e45-fa25f959b5b0\",\n \"name\": \"" + + authorName + + "\"\n }\n}"; + } + + RequestBody body = RequestBody.create(mediaType, jsonBody); + Request request = + new Request.Builder() + .url("https://" + appUrl + "/api/admin/" + entityName) + .method("POST", body) + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", token) + .build(); + + try (Response response = executeWithRetry(request)) { + if (!response.isSuccessful()) { + if (response.code() == 401) { + System.out.println( + "Create entity failed due to incorrect token. Please check the credentials"); + } + 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 entityName, String srvpath, String entityID) { MediaType mediaType = MediaType.parse("application/json"); Request request = @@ -1221,4 +1279,48 @@ public Map fetchChangelog( throw new IOException("Error fetching changelog: " + e.getMessage(), e); } } + + @Override + public String createAttachmentInActive( + String appUrl, String entityName, String facetName, String entityID) throws IOException { + String url = + "https://" + + appUrl + + "/api/admin/" + + entityName + + "(ID=" + + entityID + + ",IsActiveEntity=true)/" + + facetName + + "/" + + "AdminService.createAttachmentInActive"; + + RequestBody body = RequestBody.create("{}", MediaType.parse("application/json")); + + Request request = + new Request.Builder() + .url(url) + .post(body) + .addHeader("Authorization", token) + .addHeader("Content-Type", "application/json") + .build(); + + try (Response response = executeWithRetry(request)) { + if (!response.isSuccessful()) { + String errorBody = response.body() != null ? response.body().string() : ""; + System.out.println( + "createAttachmentInActive failed for " + + entityName + + "." + + facetName + + ". Error: " + + errorBody); + return errorBody; + } + return "Attachment created in active entity"; + } catch (IOException e) { + System.out.println("createAttachmentInActive failed: " + e.getMessage()); + return "Could not create attachment in active entity"; + } + } } diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_ActiveEntity.java b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_ActiveEntity.java new file mode 100644 index 00000000..503b8fa5 --- /dev/null +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_ActiveEntity.java @@ -0,0 +1,739 @@ +package integration.com.sap.cds.sdm; + +import static org.junit.jupiter.api.Assertions.*; + +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.*; + +/** + * Integration tests for the active entity attachment flow. + * + *

These tests cover the createAttachmentInActive action, which creates attachments directly on + * published (active) entities without going through the draft choreography. This exercises the full + * path: AdminServiceHandler โ†’ SDMAttachmentsServiceHandler (active entity detection) โ†’ ThreadLocal + * metadata storage โ†’ SDMCreateAttachmentsHandler @After update (objectId, folderId, uploadStatus). + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class IntegrationTest_ActiveEntity { + private static String token; + private static String tokenNoRoles; + private static String clientId; + private static String clientSecret; + private static String appUrl; + private static String authUrl; + private static String username; + private static String password; + private static String noSDMRoleUsername; + private static String noSDMRoleUserPassword; + private static String serviceName = "AdminService"; + private static String entityName = "Books"; + private static String entityName2 = "author"; + private static String chapterEntityName = "Chapters"; + private static String bookEntityName = "Books"; + private static String srvpath = "AdminService"; + private static ApiInterface api; + private static ApiInterface apiNoRoles; + + // Entity IDs shared across ordered tests + private static String entityID; + private static String entityID2; + private static String entityID3; + private static String bookIDForChapter; + private static String chapterID; + + // Attachment IDs discovered after active entity creation + private static String activeAttachmentID1 = ""; + private static String activeAttachmentID2 = ""; + + @BeforeAll + static void setup() throws IOException { + Properties credentialsProperties = Credentials.getCredentials(); + String tenancyModel = System.getProperty("tenancyModel"); + String tenant = System.getProperty("tenant"); + + username = credentialsProperties.getProperty("username"); + password = credentialsProperties.getProperty("password"); + noSDMRoleUsername = credentialsProperties.getProperty("noSDMRoleUsername"); + noSDMRoleUserPassword = credentialsProperties.getProperty("noSDMRoleUserPassword"); + if (tenancyModel.equals("single")) { + System.out.println("Running active entity integration tests | Single tenant Scenario"); + clientId = credentialsProperties.getProperty("clientID"); + clientSecret = credentialsProperties.getProperty("clientSecret"); + appUrl = credentialsProperties.getProperty("appUrl"); + authUrl = credentialsProperties.getProperty("authUrl"); + } else if (tenancyModel.equals("multi")) { + clientId = credentialsProperties.getProperty("clientIDMT"); + clientSecret = credentialsProperties.getProperty("clientSecretMT"); + appUrl = credentialsProperties.getProperty("appUrlMT"); + if (tenant.equals("TENANT1")) { + System.out.println( + "Running active entity integration tests | Multitenant Scenario | SDM DEV Consumer"); + authUrl = credentialsProperties.getProperty("authUrlMT1"); + } else if (tenant.equals("TENANT2")) { + System.out.println( + "Running active entity integration tests | Multitenant Scenario | Googleworkspace Consumer"); + authUrl = credentialsProperties.getProperty("authUrlMT2"); + } else { + throw new IllegalArgumentException("Invalid tenant specified: " + tenant); + } + } else { + throw new IllegalArgumentException("Invalid tenancy model specified: " + tenancyModel); + } + + String credentials = clientId + ":" + clientSecret; + String basicAuth = + "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + + OkHttpClient client = + new OkHttpClient.Builder() + .connectTimeout(120, java.util.concurrent.TimeUnit.SECONDS) + .writeTimeout(120, java.util.concurrent.TimeUnit.SECONDS) + .readTimeout(120, java.util.concurrent.TimeUnit.SECONDS) + .build(); + MediaType mediaType = MediaType.parse("text/plain"); + RequestBody body = RequestBody.create(mediaType, ""); + Request request; + + String tokenFlowFlag = System.getProperty("tokenFlow"); + if (tokenFlowFlag.equals("namedUser")) { + System.out.println("Named user token flow"); + request = + new Request.Builder() + .url( + authUrl + + "/oauth/token?grant_type=password&username=" + + username + + "&password=" + + password) + .method("POST", body) + .addHeader("Authorization", basicAuth) + .build(); + } else if (tokenFlowFlag.equals("technicalUser")) { + System.out.println("Technical user token flow"); + request = + new Request.Builder() + .url(authUrl + "/oauth/token?grant_type=client_credentials") + .method("POST", body) + .addHeader("Authorization", basicAuth) + .build(); + } else { + throw new IllegalArgumentException("Invalid token flow specified: " + tokenFlowFlag); + } + + Request requestNoRoles = + new Request.Builder() + .url( + authUrl + + "/oauth/token?grant_type=password&username=" + + noSDMRoleUsername + + "&password=" + + noSDMRoleUserPassword) + .method("POST", body) + .addHeader("Authorization", basicAuth) + .build(); + + Response response = client.newCall(request).execute(); + Response responseNoRoles = client.newCall(requestNoRoles).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); + } + if (responseNoRoles.code() != 200) { + System.out.println("Token generation failed. Response code: " + responseNoRoles.code()); + String errorBody = responseNoRoles.body().string(); + System.out.println("Error body: " + errorBody); + } + token = new ObjectMapper().readTree(response.body().string()).get("access_token").asText(); + tokenNoRoles = + new ObjectMapper().readTree(responseNoRoles.body().string()).get("access_token").asText(); + response.close(); + responseNoRoles.close(); + Map config = new HashMap<>(); + config.put("Authorization", "Bearer " + token); + Map configNoRoles = new HashMap<>(); + configNoRoles.put("Authorization", "Bearer " + tokenNoRoles); + if (tenancyModel.equals("multi")) { + api = new ApiMT(config); + apiNoRoles = new ApiMT(configNoRoles); + } else if (tenancyModel.equals("single")) { + config.put("serviceName", serviceName); + configNoRoles.put("serviceName", serviceName); + api = new Api(config); + apiNoRoles = new Api(configNoRoles); + } else { + throw new IllegalArgumentException("Invalid tenancy model specified: " + tenancyModel); + } + + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + // Pre-test cleanup: remove any stale test-fixture books (author == "author") + // from prior runs. Failures here are logged but never block the tests so + // we can still see the real test outcome. + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + cleanupStaleTestData(client); + } + + /** + * Deletes any leftover {@code Books} rows whose {@code author} field equals the test-fixture + * literal {@code "author"} โ€” both active and draft. Stale rows from a previously crashed run can + * leave the CAP draft/attachments table in a state that makes follow-up active-entity tests fail + * with "filename does not exist" errors. Running this before each suite gives every test a clean + * baseline. + * + *

Best-effort: any IO/HTTP failure is logged with a warning and the method returns so the real + * tests can still run and surface the actual error. + */ + private static void cleanupStaleTestData(OkHttpClient client) { + // MT app exposes books at /api/admin/Books; ST app at /odata/v4/AdminService/Books. + // The Api/ApiMT helpers know the right path โ€” we mirror their URL construction here. + String tenancyModel = System.getProperty("tenancyModel"); + String baseUrl = + "multi".equals(tenancyModel) + ? "https://" + appUrl + "/api/admin/" + entityName + : "https://" + appUrl + "/odata/v4/" + srvpath + "/" + entityName; + int activeDeleted = cleanupBookSet(client, baseUrl + "?$filter=author eq 'author'", true); + int draftDeleted = + cleanupBookSet( + client, baseUrl + "?$filter=author eq 'author' and IsActiveEntity eq false", false); + System.out.println( + "๐Ÿงน Pre-test cleanup: deleted " + + activeDeleted + + " active and " + + draftDeleted + + " draft fixture book(s) (author='author')"); + } + + /** + * Fetches all books matching {@code filterUrl} and deletes each by ID. Returns count successfully + * deleted. Never throws โ€” IO failures are swallowed with a warning so cleanup never blocks tests. + */ + private static int cleanupBookSet(OkHttpClient client, String filterUrl, boolean active) { + int deleted = 0; + try { + Request listReq = + new Request.Builder() + .url(filterUrl) + .get() + .addHeader("Authorization", "Bearer " + token) + .build(); + try (Response listRes = client.newCall(listReq).execute()) { + if (listRes.code() != 200) { + System.out.println( + "โš ๏ธ Cleanup list (" + + (active ? "active" : "draft") + + ") returned HTTP " + + listRes.code() + + " โ€” skipping"); + return 0; + } + ObjectMapper mapper = new ObjectMapper(); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(listRes.body().string()); + com.fasterxml.jackson.databind.JsonNode values = root.path("value"); + if (!values.isArray()) { + return 0; + } + for (com.fasterxml.jackson.databind.JsonNode book : values) { + String id = book.path("ID").asText(""); + if (id.isEmpty()) { + continue; + } + try { + String resp = + active + ? api.deleteEntity(appUrl, entityName, id) + : api.deleteEntityDraft(appUrl, entityName, id); + if ("Entity Deleted".equals(resp) || "Deleted".equals(resp)) { + deleted++; + } else { + System.out.println( + "โš ๏ธ Cleanup could not delete " + + (active ? "active" : "draft") + + " book " + + id + + " (response: " + + resp + + ")"); + } + } catch (Exception innerEx) { + System.out.println( + "โš ๏ธ Cleanup delete failed for " + + (active ? "active" : "draft") + + " book " + + id + + ": " + + innerEx.getMessage()); + } + } + } + } catch (Exception e) { + System.out.println( + "โš ๏ธ Cleanup " + + (active ? "active" : "draft") + + " list-fetch failed (continuing): " + + e.getMessage()); + } + return deleted; + } + + @Test + @Order(1) + void testCreateActiveEntityAndCreateAttachmentInActive() throws IOException { + System.out.println( + "Test (1) : Create entity, activate it, and create attachment via active entity flow"); + boolean testStatus = false; + String response = api.createEntityDraft(appUrl, entityName, entityName2, srvpath); + if (!response.equals("Could not create entity")) { + entityID = response; + response = api.saveEntityDraft(appUrl, entityName, srvpath, entityID); + if (response.equals("Saved")) { + response = api.createAttachmentInActive(appUrl, entityName, "attachments", entityID); + if (response.equals("Attachment created in active entity")) { + testStatus = true; + } + } + } + if (!testStatus) { + fail("Could not create attachment via active entity flow. Last response: " + response); + } + } + + @Test + @Order(2) + void testVerifyAttachmentExistsAfterActiveEntityCreation() throws IOException { + System.out.println( + "Test (2) : Verify attachment appears in entity metadata after active entity creation"); + boolean testStatus = false; + List> attachments = + api.fetchEntityMetadata(appUrl, entityName, "attachments", entityID); + if (attachments != null && !attachments.isEmpty()) { + activeAttachmentID1 = (String) attachments.get(0).get("ID"); + if (activeAttachmentID1 != null && !activeAttachmentID1.isEmpty()) { + testStatus = true; + } + } + if (!testStatus) { + fail( + "Attachment not found in entity metadata after createAttachmentInActive. Attachments: " + + attachments); + } + } + + @Test + @Order(3) + void testVerifySDMMetadataOnActiveEntityAttachment() throws IOException { + System.out.println( + "Test (3) : Verify SDM metadata (objectId, folderId, uploadStatus) is set correctly" + + " โ€” tests ThreadLocal mechanism and @After handler"); + Map metadata = + api.fetchMetadata(appUrl, entityName, "attachments", entityID, activeAttachmentID1); + assertNotNull(metadata, "Metadata must not be null"); + Object objectId = metadata.get("objectId"); + assertNotNull( + objectId, "objectId must be populated by SDMCreateAttachmentsHandler @After handler"); + assertFalse(objectId.toString().isEmpty(), "objectId must not be empty"); + Object folderId = metadata.get("folderId"); + assertNotNull(folderId, "folderId must be set after active entity upload"); + assertFalse(folderId.toString().isEmpty(), "folderId must not be empty"); + Object uploadStatus = metadata.get("uploadStatus"); + assertEquals( + "Success", uploadStatus, "uploadStatus must be Success after active entity upload"); + } + + @Test + @Order(4) + void testReadContentOfActiveEntityAttachment() throws IOException { + System.out.println( + "Test (4) : Read attachment content from active entity โ€” confirms file is in SDM"); + String response = + api.readAttachment(appUrl, entityName, "attachments", entityID, activeAttachmentID1); + if (!response.equals("OK")) { + fail("Could not read attachment content. Response: " + response); + } + } + + @Test + @Order(5) + void testCreateAttachmentInActiveWithoutSDMRole() throws IOException { + System.out.println( + "Test (5) : Create attachment in active entity without SDM role โ€” expect 500"); + boolean testStatus = false; + String response = api.createEntityDraft(appUrl, entityName, entityName2, srvpath); + if (!response.equals("Could not create entity")) { + entityID2 = response; + String saveResponse = api.saveEntityDraft(appUrl, entityName, srvpath, entityID2); + if (saveResponse.equals("Saved")) { + String createResponse = + apiNoRoles.createAttachmentInActive(appUrl, entityName, "attachments", entityID2); + if (createResponse.contains("\"code\":\"500\"") + && createResponse.contains( + "You do not have the required permissions to upload attachments")) { + testStatus = true; + } else { + System.out.println( + "Unexpected response from no-roles user createAttachmentInActive: " + createResponse); + } + } + } + if (!testStatus) { + fail("Attachment was created without SDM role, or entity setup failed"); + } + } + + @Test + @Order(6) + void testCreateAttachmentInActiveOnEntityWithExistingDraftAttachments() throws IOException { + System.out.println( + "Test (6) : Active entity flow coexists with draft-uploaded attachments on same entity"); + boolean testStatus = false; + String response = api.createEntityDraft(appUrl, entityName, entityName2, srvpath); + if (!response.equals("Could not create entity")) { + entityID3 = response; + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource("sample.pdf").getFile()); + Map postData = new HashMap<>(); + postData.put("up__ID", entityID3); + postData.put("mimeType", "application/pdf"); + postData.put("createdAt", new Date().toString()); + postData.put("createdBy", "test@test.com"); + postData.put("modifiedBy", "test@test.com"); + List createResponse = + api.createAttachment( + appUrl, entityName, "attachments", entityID3, srvpath, postData, file); + if (createResponse.get(0).equals("Attachment created")) { + response = api.saveEntityDraft(appUrl, entityName, srvpath, entityID3); + if (response.equals("Saved")) { + // Now add an attachment via the active entity flow on the same entity + String activeResponse = + api.createAttachmentInActive(appUrl, entityName, "attachments", entityID3); + if (activeResponse.equals("Attachment created in active entity")) { + List> attachments = + api.fetchEntityMetadata(appUrl, entityName, "attachments", entityID3); + if (attachments != null && attachments.size() == 2) { + // Verify the active-flow attachment also has objectId + boolean allHaveObjectId = + attachments.stream() + .allMatch( + m -> + m.get("objectId") != null && !m.get("objectId").toString().isEmpty()); + if (allHaveObjectId) { + testStatus = true; + } + } + } + } + } + } + if (!testStatus) { + fail("Active entity flow did not coexist properly with draft-uploaded attachments"); + } + } + + @Test + @Order(7) + void testCreateMultipleAttachmentsViaActiveEntityFlow() throws IOException { + System.out.println( + "Test (7) : Create second attachment via active entity flow on same entity โ€” timestamp-based" + + " filename prevents duplicates"); + boolean testStatus = false; + String response = api.createAttachmentInActive(appUrl, entityName, "attachments", entityID); + if (response.equals("Attachment created in active entity")) { + List> attachments = + api.fetchEntityMetadata(appUrl, entityName, "attachments", entityID); + if (attachments != null && attachments.size() == 2) { + // Discover the second attachment ID (the one that isn't activeAttachmentID1) + for (Map att : attachments) { + String id = (String) att.get("ID"); + if (id != null && !id.equals(activeAttachmentID1)) { + activeAttachmentID2 = id; + break; + } + } + if (!activeAttachmentID2.isEmpty()) { + testStatus = true; + } + } + } + if (!testStatus) { + fail( + "Could not create second attachment via active entity flow or count mismatch. Response: " + + response); + } + } + + @Test + @Order(8) + void testCreateAttachmentInActiveOnReferencesFacet() throws IOException { + System.out.println("Test (8) : Create attachment in active entity on 'references' facet"); + boolean testStatus = false; + String response = api.createAttachmentInActive(appUrl, entityName, "references", entityID); + if (response.equals("Attachment created in active entity")) { + List> attachments = + api.fetchEntityMetadata(appUrl, entityName, "references", entityID); + if (attachments != null && !attachments.isEmpty()) { + String refAttachmentID = (String) attachments.get(0).get("ID"); + if (refAttachmentID != null && !refAttachmentID.isEmpty()) { + Map metadata = + api.fetchMetadata(appUrl, entityName, "references", entityID, refAttachmentID); + Object objectId = metadata.get("objectId"); + if (objectId != null && !objectId.toString().isEmpty()) { + testStatus = true; + } + } + } + } + if (!testStatus) { + fail( + "Could not create attachment in active entity on references facet. Response: " + + response); + } + } + + @Test + @Order(9) + void testCreateAttachmentInActiveOnFootnotesFacet() throws IOException { + System.out.println("Test (9) : Create attachment in active entity on 'footnotes' facet"); + boolean testStatus = false; + String response = api.createAttachmentInActive(appUrl, entityName, "footnotes", entityID); + if (response.equals("Attachment created in active entity")) { + List> attachments = + api.fetchEntityMetadata(appUrl, entityName, "footnotes", entityID); + if (attachments != null && !attachments.isEmpty()) { + String footnoteAttachmentID = (String) attachments.get(0).get("ID"); + if (footnoteAttachmentID != null && !footnoteAttachmentID.isEmpty()) { + Map metadata = + api.fetchMetadata(appUrl, entityName, "footnotes", entityID, footnoteAttachmentID); + Object objectId = metadata.get("objectId"); + if (objectId != null && !objectId.toString().isEmpty()) { + testStatus = true; + } + } + } + } + if (!testStatus) { + fail( + "Could not create attachment in active entity on footnotes facet. Response: " + response); + } + } + + @Test + @Order(10) + void testDeleteAttachmentCreatedViaActiveEntityFlow() throws IOException { + System.out.println("Test (10) : Delete attachment that was created via active entity flow"); + boolean testStatus = false; + String response = api.editEntityDraft(appUrl, entityName, srvpath, entityID); + if (response == "Entity in draft mode") { + response = + api.deleteAttachment(appUrl, entityName, "attachments", entityID, activeAttachmentID1); + if (response == "Deleted") { + response = api.saveEntityDraft(appUrl, entityName, srvpath, entityID); + if (response == "Saved") { + response = + api.readAttachment(appUrl, entityName, "attachments", entityID, activeAttachmentID1); + if (response.equals("Could not read Attachment")) { + testStatus = true; + } + } + } + } + if (!testStatus) { + fail( + "Could not delete attachment created via active entity flow. Last response: " + response); + } + } + + @Test + @Order(11) + void testRenameAttachmentCreatedViaActiveEntityFlow() throws IOException { + System.out.println("Test (11) : Rename attachment that was created via active entity flow"); + boolean testStatus = false; + String response = api.editEntityDraft(appUrl, entityName, srvpath, entityID); + if (response == "Entity in draft mode") { + String newName = "renamed-active-attachment.txt"; + response = + api.renameAttachment( + appUrl, entityName, "attachments", entityID, activeAttachmentID2, newName); + if (response.equals("Renamed")) { + response = api.saveEntityDraft(appUrl, entityName, srvpath, entityID); + if (response.equals("Saved")) { + Map metadata = + api.fetchMetadata(appUrl, entityName, "attachments", entityID, activeAttachmentID2); + if (newName.equals(metadata.get("fileName"))) { + testStatus = true; + } + } + } else { + api.saveEntityDraft(appUrl, entityName, srvpath, entityID); + } + } + if (!testStatus) { + fail("Could not rename attachment created via active entity flow"); + } + } + + @Test + @Order(12) + void testDownloadAttachmentCreatedViaActiveEntityFlow() throws IOException { + System.out.println("Test (12) : Batch download attachment created via active entity flow"); + boolean testStatus = false; + List ids = new ArrayList<>(); + ids.add(activeAttachmentID2); + String response = + api.downloadSelectedAttachments(appUrl, entityName, "attachments", entityID, ids); + if (response != null && !response.isEmpty()) { + testStatus = true; + } + if (!testStatus) { + fail("Could not download attachment created via active entity flow. Response: " + response); + } + } + + @Test + @Order(13) + void testCopyActiveEntityAttachmentToAnotherEntity() throws IOException { + System.out.println( + "Test (13) : Copy attachment (created via active entity flow) to another entity"); + boolean testStatus = false; + Map metadata = + api.fetchMetadata(appUrl, entityName, "attachments", entityID, activeAttachmentID2); + if (!metadata.containsKey("objectId")) { + fail("Source attachment metadata does not contain objectId"); + } + String sourceObjectId = metadata.get("objectId").toString(); + // Use entityID3 (already has attachments) as the copy target + String response = api.editEntityDraft(appUrl, entityName, srvpath, entityID3); + if (response == "Entity in draft mode") { + List objectIdsToCopy = new ArrayList<>(); + objectIdsToCopy.add(sourceObjectId); + String copyResponse = + api.copyAttachment(appUrl, entityName, "attachments", entityID3, objectIdsToCopy); + if (copyResponse.equals("Attachments copied successfully")) { + response = api.saveEntityDraft(appUrl, entityName, srvpath, entityID3); + if (response.equals("Saved")) { + List> copiedAttachments = + api.fetchEntityMetadata(appUrl, entityName, "attachments", entityID3); + // entityID3 had 2 attachments (1 draft + 1 active) โ€” after copy it should have 3 + if (copiedAttachments != null && copiedAttachments.size() >= 3) { + boolean copiedHasObjectId = + copiedAttachments.stream() + .anyMatch( + m -> m.get("objectId") != null && !m.get("objectId").toString().isEmpty()); + if (copiedHasObjectId) { + testStatus = true; + } + } + } + } + } + if (!testStatus) { + fail("Could not copy attachment created via active entity flow to another entity"); + } + } + + @Test + @Order(14) + void testNestedChapterEntityCreateAttachmentInActive() throws IOException { + System.out.println( + "Test (14) : Create attachment in active entity on nested Chapters entity โ€” exercises" + + " extractParentId CQN path traversal in AdminServiceHandler"); + boolean testStatus = false; + // Create a book first + String bookResponse = api.createEntityDraft(appUrl, bookEntityName, entityName2, srvpath); + if (!bookResponse.equals("Could not create entity")) { + bookIDForChapter = bookResponse; + // Create a chapter inside the book + String chapterResponse = + api.createEntityDraft(appUrl, chapterEntityName, entityName2, srvpath, bookIDForChapter); + if (!chapterResponse.equals("Could not create entity")) { + chapterID = chapterResponse; + // Save the book (this activates both book and chapter) + String saveResponse = + api.saveEntityDraft(appUrl, bookEntityName, srvpath, bookIDForChapter); + if (saveResponse.equals("Saved")) { + // Verify chapter exists as active entity + String checkResponse = api.checkEntity(appUrl, chapterEntityName, chapterID); + if (checkResponse.equals("Entity exists")) { + // Call createAttachmentInActive on the active chapter + String activeResponse = + api.createAttachmentInActive(appUrl, chapterEntityName, "attachments", chapterID); + if (activeResponse.equals("Attachment created in active entity")) { + List> attachments = + api.fetchEntityMetadata(appUrl, chapterEntityName, "attachments", chapterID); + if (attachments != null && !attachments.isEmpty()) { + String chapterAttachmentID = (String) attachments.get(0).get("ID"); + if (chapterAttachmentID != null && !chapterAttachmentID.isEmpty()) { + Map chapterMetadata = + api.fetchMetadata( + appUrl, chapterEntityName, "attachments", chapterID, chapterAttachmentID); + Object objectId = chapterMetadata.get("objectId"); + if (objectId != null && !objectId.toString().isEmpty()) { + testStatus = true; + } + } + } + } else { + System.out.println("createAttachmentInActive on chapter failed: " + activeResponse); + } + } + } + } + } + if (!testStatus) { + fail("Could not create attachment via active entity flow on nested Chapters entity"); + } + } + + @Test + @Order(15) + void testDeleteEntityWithActiveEntityAttachments() { + System.out.println( + "Test (15) : Delete entity that has attachments created via active entity flow"); + boolean testStatus = false; + String response = api.deleteEntity(appUrl, entityName, entityID); + if (response == "Entity Deleted") { + response = api.checkEntity(appUrl, entityName, entityID); + if (response.equals("Entity doesn't exist")) { + testStatus = true; + } + } + if (!testStatus) { + fail("Could not delete entity with active entity attachments. Response: " + response); + } + } + + @Test + @Order(16) + void testCleanup() { + System.out.println("Test (16) : Cleanup remaining test entities"); + boolean testStatus = true; + if (entityID2 != null && !entityID2.isEmpty()) { + String response = api.deleteEntity(appUrl, entityName, entityID2); + if (response != "Entity Deleted") { + System.out.println("Warning: could not delete entityID2: " + response); + testStatus = false; + } + } + if (entityID3 != null && !entityID3.isEmpty()) { + String response = api.deleteEntity(appUrl, entityName, entityID3); + if (response != "Entity Deleted") { + System.out.println("Warning: could not delete entityID3: " + response); + testStatus = false; + } + } + // Deleting the book also removes the chapter (composition) + if (bookIDForChapter != null && !bookIDForChapter.isEmpty()) { + String response = api.deleteEntity(appUrl, bookEntityName, bookIDForChapter); + if (response != "Entity Deleted") { + System.out.println("Warning: could not delete bookIDForChapter: " + response); + testStatus = false; + } + } + if (!testStatus) { + fail("Cleanup failed โ€” some test entities could not be deleted"); + } + } +} diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_ActiveEntity_Chapters_MultipleFacet.java b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_ActiveEntity_Chapters_MultipleFacet.java new file mode 100644 index 00000000..a573ed95 --- /dev/null +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_ActiveEntity_Chapters_MultipleFacet.java @@ -0,0 +1,571 @@ +package integration.com.sap.cds.sdm; + +import static org.junit.jupiter.api.Assertions.*; + +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.*; + +/** + * Integration tests for the active entity attachment flow on nested Chapters entities across + * multiple facets. + * + *

Exercises {@code createAttachmentInActive} on a Chapter (child composition of Book) for every + * facet ({@code attachments}, {@code references}, {@code footnotes}). This targets the + * extractParentId CQN path traversal in AdminServiceHandler and confirms the active-entity flow + * handles nested compositions correctly. + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class IntegrationTest_ActiveEntity_Chapters_MultipleFacet { + private static String token; + private static String tokenNoRoles; + private static String clientId; + private static String clientSecret; + private static String appUrl; + private static String authUrl; + private static String username; + private static String password; + private static String noSDMRoleUsername; + private static String noSDMRoleUserPassword; + private static String serviceName = "AdminService"; + private static String bookEntityName = "Books"; + private static String chapterEntityName = "Chapters"; + private static String entityName2 = "author"; + private static String srvpath = "AdminService"; + private static String[] facet = {"attachments", "references", "footnotes"}; + // Distinct author-name marker for this suite so its cleanup only touches its own books and + // doesn't collide with sibling ActiveEntity matrix jobs. + private static final String AUTHOR_NAME = "author-active-chapters-multifacet"; + private static ApiInterface api; + private static ApiInterface apiNoRoles; + + // Entity IDs shared across ordered tests + private static String bookID; + private static String chapterID; + private static String bookID2; + private static String chapterID2; + private static String bookID3; + private static String chapterID3; + + // Attachment IDs (one per facet) discovered after active entity creation + private static String[] chapterAttachmentIDs = new String[facet.length]; + + @BeforeAll + static void setup() throws IOException { + Properties credentialsProperties = Credentials.getCredentials(); + String tenancyModel = System.getProperty("tenancyModel"); + String tenant = System.getProperty("tenant"); + + username = credentialsProperties.getProperty("username"); + password = credentialsProperties.getProperty("password"); + noSDMRoleUsername = credentialsProperties.getProperty("noSDMRoleUsername"); + noSDMRoleUserPassword = credentialsProperties.getProperty("noSDMRoleUserPassword"); + if (tenancyModel.equals("single")) { + System.out.println( + "Running active entity multi-facet chapter integration tests | Single tenant Scenario"); + clientId = credentialsProperties.getProperty("clientID"); + clientSecret = credentialsProperties.getProperty("clientSecret"); + appUrl = credentialsProperties.getProperty("appUrl"); + authUrl = credentialsProperties.getProperty("authUrl"); + } else if (tenancyModel.equals("multi")) { + clientId = credentialsProperties.getProperty("clientIDMT"); + clientSecret = credentialsProperties.getProperty("clientSecretMT"); + appUrl = credentialsProperties.getProperty("appUrlMT"); + if (tenant.equals("TENANT1")) { + System.out.println( + "Running active entity multi-facet chapter integration tests | Multitenant | SDM DEV Consumer"); + authUrl = credentialsProperties.getProperty("authUrlMT1"); + } else if (tenant.equals("TENANT2")) { + System.out.println( + "Running active entity multi-facet chapter integration tests | Multitenant | Googleworkspace Consumer"); + authUrl = credentialsProperties.getProperty("authUrlMT2"); + } else { + throw new IllegalArgumentException("Invalid tenant specified: " + tenant); + } + } else { + throw new IllegalArgumentException("Invalid tenancy model specified: " + tenancyModel); + } + + String credentials = clientId + ":" + clientSecret; + String basicAuth = + "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + + OkHttpClient client = + new OkHttpClient.Builder() + .connectTimeout(120, java.util.concurrent.TimeUnit.SECONDS) + .writeTimeout(120, java.util.concurrent.TimeUnit.SECONDS) + .readTimeout(120, java.util.concurrent.TimeUnit.SECONDS) + .build(); + MediaType mediaType = MediaType.parse("text/plain"); + RequestBody body = RequestBody.create(mediaType, ""); + Request request; + + String tokenFlowFlag = System.getProperty("tokenFlow"); + if (tokenFlowFlag.equals("namedUser")) { + System.out.println("Named user token flow"); + request = + new Request.Builder() + .url( + authUrl + + "/oauth/token?grant_type=password&username=" + + username + + "&password=" + + password) + .method("POST", body) + .addHeader("Authorization", basicAuth) + .build(); + } else if (tokenFlowFlag.equals("technicalUser")) { + System.out.println("Technical user token flow"); + request = + new Request.Builder() + .url(authUrl + "/oauth/token?grant_type=client_credentials") + .method("POST", body) + .addHeader("Authorization", basicAuth) + .build(); + } else { + throw new IllegalArgumentException("Invalid token flow specified: " + tokenFlowFlag); + } + + Request requestNoRoles = + new Request.Builder() + .url( + authUrl + + "/oauth/token?grant_type=password&username=" + + noSDMRoleUsername + + "&password=" + + noSDMRoleUserPassword) + .method("POST", body) + .addHeader("Authorization", basicAuth) + .build(); + + Response response = client.newCall(request).execute(); + Response responseNoRoles = client.newCall(requestNoRoles).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); + } + if (responseNoRoles.code() != 200) { + System.out.println("Token generation failed. Response code: " + responseNoRoles.code()); + String errorBody = responseNoRoles.body().string(); + System.out.println("Error body: " + errorBody); + } + token = new ObjectMapper().readTree(response.body().string()).get("access_token").asText(); + tokenNoRoles = + new ObjectMapper().readTree(responseNoRoles.body().string()).get("access_token").asText(); + response.close(); + responseNoRoles.close(); + Map config = new HashMap<>(); + config.put("Authorization", "Bearer " + token); + Map configNoRoles = new HashMap<>(); + configNoRoles.put("Authorization", "Bearer " + tokenNoRoles); + if (tenancyModel.equals("multi")) { + api = new ApiMT(config); + apiNoRoles = new ApiMT(configNoRoles); + } else if (tenancyModel.equals("single")) { + config.put("serviceName", serviceName); + configNoRoles.put("serviceName", serviceName); + api = new Api(config); + apiNoRoles = new Api(configNoRoles); + } else { + throw new IllegalArgumentException("Invalid tenancy model specified: " + tenancyModel); + } + + // Pre-test cleanup: remove any stale test-fixture books (author == "author") from prior runs. + // Failures here are logged but never block the tests so we can still see the real test outcome. + cleanupStaleTestData(client); + } + + /** + * Deletes any leftover {@code Books} rows whose {@code author} field equals the test-fixture + * literal {@code "author"} โ€” both active and draft. Deleting a Book cascades to its Chapters, so + * this also clears stale chapter fixtures. + */ + private static void cleanupStaleTestData(OkHttpClient client) { + String tenancyModel = System.getProperty("tenancyModel"); + String baseUrl = + "multi".equals(tenancyModel) + ? "https://" + appUrl + "/api/admin/" + bookEntityName + : "https://" + appUrl + "/odata/v4/" + srvpath + "/" + bookEntityName; + int activeDeleted = + cleanupBookSet(client, baseUrl + "?$filter=author/name eq '" + AUTHOR_NAME + "'", true); + int draftDeleted = + cleanupBookSet( + client, + baseUrl + "?$filter=author/name eq '" + AUTHOR_NAME + "' and IsActiveEntity eq false", + false); + System.out.println( + "๐Ÿงน Pre-test cleanup: deleted " + + activeDeleted + + " active and " + + draftDeleted + + " draft fixture book(s) (author='author')"); + } + + /** See sibling test file โ€” same best-effort cleanup helper. */ + private static int cleanupBookSet(OkHttpClient client, String filterUrl, boolean active) { + int deleted = 0; + try { + Request listReq = + new Request.Builder() + .url(filterUrl) + .get() + .addHeader("Authorization", "Bearer " + token) + .build(); + try (Response listRes = client.newCall(listReq).execute()) { + if (listRes.code() != 200) { + System.out.println( + "โš ๏ธ Cleanup list (" + + (active ? "active" : "draft") + + ") returned HTTP " + + listRes.code() + + " โ€” skipping"); + return 0; + } + ObjectMapper mapper = new ObjectMapper(); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(listRes.body().string()); + com.fasterxml.jackson.databind.JsonNode values = root.path("value"); + if (!values.isArray()) { + return 0; + } + for (com.fasterxml.jackson.databind.JsonNode book : values) { + String id = book.path("ID").asText(""); + if (id.isEmpty()) { + continue; + } + try { + String resp = + active + ? api.deleteEntity(appUrl, bookEntityName, id) + : api.deleteEntityDraft(appUrl, bookEntityName, id); + if ("Entity Deleted".equals(resp) || "Deleted".equals(resp)) { + deleted++; + } else { + System.out.println( + "โš ๏ธ Cleanup could not delete " + + (active ? "active" : "draft") + + " book " + + id + + " (response: " + + resp + + ")"); + } + } catch (Exception innerEx) { + System.out.println( + "โš ๏ธ Cleanup delete failed for " + + (active ? "active" : "draft") + + " book " + + id + + ": " + + innerEx.getMessage()); + } + } + } + } catch (Exception e) { + System.out.println( + "โš ๏ธ Cleanup " + + (active ? "active" : "draft") + + " list-fetch failed (continuing): " + + e.getMessage()); + } + return deleted; + } + + /** + * Creates a Book + nested Chapter and saves the draft so both become active. Returns an {@code + * [bookID, chapterID]} array, or {@code null} if any step fails. + */ + private static String[] createAndActivateBookWithChapter() { + String bookResponse = + api.createEntityDraftWithAuthor(appUrl, bookEntityName, entityName2, srvpath, AUTHOR_NAME); + if (bookResponse.equals("Could not create entity")) { + return null; + } + String bId = bookResponse; + String chapterResponse = + api.createEntityDraft(appUrl, chapterEntityName, entityName2, srvpath, bId); + if (chapterResponse.equals("Could not create entity")) { + return null; + } + String cId = chapterResponse; + String saveResponse = api.saveEntityDraft(appUrl, bookEntityName, srvpath, bId); + if (!saveResponse.equals("Saved")) { + return null; + } + String checkResponse = api.checkEntity(appUrl, chapterEntityName, cId); + if (!checkResponse.equals("Entity exists")) { + return null; + } + return new String[] {bId, cId}; + } + + @Test + @Order(1) + void testCreateAttachmentInActiveOnChapterAllFacets() throws IOException { + System.out.println( + "Test (1) : Create Book+Chapter, activate, then create attachment on all chapter facets" + + " via active entity flow"); + boolean testStatus = false; + String[] ids = createAndActivateBookWithChapter(); + if (ids != null) { + bookID = ids[0]; + chapterID = ids[1]; + int successCount = 0; + for (String f : facet) { + String r = api.createAttachmentInActive(appUrl, chapterEntityName, f, chapterID); + if (r.equals("Attachment created in active entity")) { + successCount++; + } else { + System.out.println("createAttachmentInActive failed on chapter facet '" + f + "': " + r); + } + } + if (successCount == facet.length) { + testStatus = true; + } + } + if (!testStatus) { + fail("Could not create attachment via active entity flow on all chapter facets"); + } + } + + @Test + @Order(2) + void testVerifyChapterAttachmentsExistOnAllFacets() throws IOException { + System.out.println( + "Test (2) : Verify attachment appears in metadata on every chapter facet after active" + + " entity create"); + boolean testStatus = true; + for (int i = 0; i < facet.length; i++) { + List> attachments = + api.fetchEntityMetadata(appUrl, chapterEntityName, facet[i], chapterID); + if (attachments == null || attachments.isEmpty()) { + testStatus = false; + System.out.println("No attachments found on chapter facet '" + facet[i] + "'"); + continue; + } + String id = (String) attachments.get(0).get("ID"); + if (id == null || id.isEmpty()) { + testStatus = false; + continue; + } + chapterAttachmentIDs[i] = id; + } + if (!testStatus) { + fail( + "Attachment not found in chapter metadata on one or more facets after" + + " createAttachmentInActive"); + } + } + + @Test + @Order(3) + void testVerifySDMMetadataOnChapterAllFacets() throws IOException { + System.out.println( + "Test (3) : Verify SDM metadata (objectId, folderId, uploadStatus) on every chapter facet"); + for (int i = 0; i < facet.length; i++) { + Map metadata = + api.fetchMetadata( + appUrl, chapterEntityName, facet[i], chapterID, chapterAttachmentIDs[i]); + assertNotNull(metadata, "Metadata must not be null for chapter facet '" + facet[i] + "'"); + Object objectId = metadata.get("objectId"); + assertNotNull( + objectId, + "objectId must be populated on chapter facet '" + facet[i] + "' by @After handler"); + assertFalse( + objectId.toString().isEmpty(), + "objectId must not be empty on chapter facet '" + facet[i] + "'"); + Object folderId = metadata.get("folderId"); + assertNotNull(folderId, "folderId must be set on chapter facet '" + facet[i] + "'"); + assertFalse( + folderId.toString().isEmpty(), + "folderId must not be empty on chapter facet '" + facet[i] + "'"); + Object uploadStatus = metadata.get("uploadStatus"); + assertEquals( + "Success", + uploadStatus, + "uploadStatus must be Success on chapter facet '" + facet[i] + "'"); + } + } + + @Test + @Order(4) + void testReadContentOfChapterAttachmentsOnAllFacets() throws IOException { + System.out.println( + "Test (4) : Read chapter attachment content on every facet โ€” confirms file is in SDM"); + for (int i = 0; i < facet.length; i++) { + String response = + api.readAttachment( + appUrl, chapterEntityName, facet[i], chapterID, chapterAttachmentIDs[i]); + if (!response.equals("OK")) { + fail( + "Could not read chapter attachment content on facet '" + + facet[i] + + "'. Response: " + + response); + } + } + } + + @Test + @Order(5) + void testCreateAttachmentInActiveOnChapterWithoutSDMRoleAllFacets() throws IOException { + System.out.println( + "Test (5) : Create chapter attachment via active entity without SDM role on every facet โ€”" + + " expect 500"); + boolean testStatus = false; + String[] ids = createAndActivateBookWithChapter(); + if (ids != null) { + bookID2 = ids[0]; + chapterID2 = ids[1]; + int rejectedCount = 0; + for (String f : facet) { + String createResponse = + apiNoRoles.createAttachmentInActive(appUrl, chapterEntityName, f, chapterID2); + if (createResponse.contains("\"code\":\"500\"") + && createResponse.contains( + "You do not have the required permissions to upload attachments")) { + rejectedCount++; + } else { + System.out.println( + "Unexpected response from no-roles user on chapter facet '" + + f + + "': " + + createResponse); + } + } + if (rejectedCount == facet.length) { + testStatus = true; + } + } + if (!testStatus) { + fail( + "Chapter attachment was created without SDM role on one or more facets, or entity" + + " setup failed"); + } + } + + @Test + @Order(6) + void testCreateAttachmentInActiveCoexistsWithDraftUploadsOnChapterAllFacets() throws IOException { + System.out.println( + "Test (6) : Active entity flow coexists with draft-uploaded chapter attachments across" + + " all facets"); + boolean testStatus = false; + // Create a fresh book+chapter draft (kept in draft mode so we can upload draft attachments) + String bookResponse = + api.createEntityDraftWithAuthor(appUrl, bookEntityName, entityName2, srvpath, AUTHOR_NAME); + if (!bookResponse.equals("Could not create entity")) { + bookID3 = bookResponse; + String chapterResponse = + api.createEntityDraft(appUrl, chapterEntityName, entityName2, srvpath, bookID3); + if (!chapterResponse.equals("Could not create entity")) { + chapterID3 = chapterResponse; + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource("sample.pdf").getFile()); + int draftUploadCount = 0; + for (String f : facet) { + Map postData = new HashMap<>(); + postData.put("up__ID", chapterID3); + postData.put("mimeType", "application/pdf"); + postData.put("createdAt", new Date().toString()); + postData.put("createdBy", "test@test.com"); + postData.put("modifiedBy", "test@test.com"); + List createResponse = + api.createAttachment( + appUrl, chapterEntityName, f, chapterID3, srvpath, postData, file); + if (createResponse.get(0).equals("Attachment created")) { + draftUploadCount++; + } + } + if (draftUploadCount == facet.length) { + String response = api.saveEntityDraft(appUrl, bookEntityName, srvpath, bookID3); + if (response.equals("Saved")) { + int activeCount = 0; + for (String f : facet) { + String activeResponse = + api.createAttachmentInActive(appUrl, chapterEntityName, f, chapterID3); + if (activeResponse.equals("Attachment created in active entity")) { + activeCount++; + } + } + if (activeCount == facet.length) { + boolean allValid = true; + for (String f : facet) { + List> attachments = + api.fetchEntityMetadata(appUrl, chapterEntityName, f, chapterID3); + if (attachments == null || attachments.size() != 2) { + allValid = false; + break; + } + boolean allHaveObjectId = + attachments.stream() + .allMatch( + m -> + m.get("objectId") != null + && !m.get("objectId").toString().isEmpty()); + if (!allHaveObjectId) { + allValid = false; + break; + } + } + if (allValid) { + testStatus = true; + } + } + } + } + } + } + if (!testStatus) { + fail( + "Active entity flow did not coexist properly with draft-uploaded chapter attachments" + + " on all facets"); + } + } + + @Test + @Order(7) + void testDeleteBookRemovesChapterWithActiveAttachments() { + System.out.println( + "Test (7) : Delete book โ€” cascade should remove chapter + its active-entity attachments"); + boolean testStatus = false; + String response = api.deleteEntity(appUrl, bookEntityName, bookID); + if (response == "Entity Deleted") { + response = api.checkEntity(appUrl, chapterEntityName, chapterID); + if (response.equals("Entity doesn't exist")) { + testStatus = true; + } + } + if (!testStatus) { + fail("Could not delete book with chapter's active entity attachments. Response: " + response); + } + } + + @Test + @Order(8) + void testCleanup() { + System.out.println("Test (8) : Cleanup remaining test books"); + boolean testStatus = true; + if (bookID2 != null && !bookID2.isEmpty()) { + String response = api.deleteEntity(appUrl, bookEntityName, bookID2); + if (response != "Entity Deleted") { + System.out.println("Warning: could not delete bookID2: " + response); + testStatus = false; + } + } + if (bookID3 != null && !bookID3.isEmpty()) { + String response = api.deleteEntity(appUrl, bookEntityName, bookID3); + if (response != "Entity Deleted") { + System.out.println("Warning: could not delete bookID3: " + response); + testStatus = false; + } + } + if (!testStatus) { + fail("Cleanup failed โ€” some test books could not be deleted"); + } + } +} diff --git a/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_ActiveEntity_MultipleFacet.java b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_ActiveEntity_MultipleFacet.java new file mode 100644 index 00000000..fdb1f702 --- /dev/null +++ b/sdm/src/test/java/integration/com/sap/cds/sdm/IntegrationTest_ActiveEntity_MultipleFacet.java @@ -0,0 +1,523 @@ +package integration.com.sap.cds.sdm; + +import static org.junit.jupiter.api.Assertions.*; + +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.*; + +/** + * Integration tests for the active entity attachment flow across multiple facets. + * + *

Mirrors {@link IntegrationTest_ActiveEntity} but exercises the {@code + * createAttachmentInActive} action on every facet ({@code attachments}, {@code references}, {@code + * footnotes}) of the Books entity in sequence, so a regression in one facet doesn't hide behind the + * others. + */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class IntegrationTest_ActiveEntity_MultipleFacet { + private static String token; + private static String tokenNoRoles; + private static String clientId; + private static String clientSecret; + private static String appUrl; + private static String authUrl; + private static String username; + private static String password; + private static String noSDMRoleUsername; + private static String noSDMRoleUserPassword; + private static String serviceName = "AdminService"; + private static String entityName = "Books"; + private static String entityName2 = "author"; + private static String srvpath = "AdminService"; + private static String[] facet = {"attachments", "references", "footnotes"}; + // Distinct author-name marker for this suite so its cleanup only touches its own books and + // doesn't collide with sibling ActiveEntity matrix jobs. + private static final String AUTHOR_NAME = "author-active-multifacet"; + private static ApiInterface api; + private static ApiInterface apiNoRoles; + + // Entity IDs shared across ordered tests + private static String entityID; + private static String entityID2; + private static String entityID3; + + // Attachment IDs (one per facet) discovered after active entity creation + private static String[] activeAttachmentIDs = new String[facet.length]; + + @BeforeAll + static void setup() throws IOException { + Properties credentialsProperties = Credentials.getCredentials(); + String tenancyModel = System.getProperty("tenancyModel"); + String tenant = System.getProperty("tenant"); + + username = credentialsProperties.getProperty("username"); + password = credentialsProperties.getProperty("password"); + noSDMRoleUsername = credentialsProperties.getProperty("noSDMRoleUsername"); + noSDMRoleUserPassword = credentialsProperties.getProperty("noSDMRoleUserPassword"); + if (tenancyModel.equals("single")) { + System.out.println( + "Running active entity multi-facet integration tests | Single tenant Scenario"); + clientId = credentialsProperties.getProperty("clientID"); + clientSecret = credentialsProperties.getProperty("clientSecret"); + appUrl = credentialsProperties.getProperty("appUrl"); + authUrl = credentialsProperties.getProperty("authUrl"); + } else if (tenancyModel.equals("multi")) { + clientId = credentialsProperties.getProperty("clientIDMT"); + clientSecret = credentialsProperties.getProperty("clientSecretMT"); + appUrl = credentialsProperties.getProperty("appUrlMT"); + if (tenant.equals("TENANT1")) { + System.out.println( + "Running active entity multi-facet integration tests | Multitenant | SDM DEV Consumer"); + authUrl = credentialsProperties.getProperty("authUrlMT1"); + } else if (tenant.equals("TENANT2")) { + System.out.println( + "Running active entity multi-facet integration tests | Multitenant | Googleworkspace Consumer"); + authUrl = credentialsProperties.getProperty("authUrlMT2"); + } else { + throw new IllegalArgumentException("Invalid tenant specified: " + tenant); + } + } else { + throw new IllegalArgumentException("Invalid tenancy model specified: " + tenancyModel); + } + + String credentials = clientId + ":" + clientSecret; + String basicAuth = + "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + + OkHttpClient client = + new OkHttpClient.Builder() + .connectTimeout(120, java.util.concurrent.TimeUnit.SECONDS) + .writeTimeout(120, java.util.concurrent.TimeUnit.SECONDS) + .readTimeout(120, java.util.concurrent.TimeUnit.SECONDS) + .build(); + MediaType mediaType = MediaType.parse("text/plain"); + RequestBody body = RequestBody.create(mediaType, ""); + Request request; + + String tokenFlowFlag = System.getProperty("tokenFlow"); + if (tokenFlowFlag.equals("namedUser")) { + System.out.println("Named user token flow"); + request = + new Request.Builder() + .url( + authUrl + + "/oauth/token?grant_type=password&username=" + + username + + "&password=" + + password) + .method("POST", body) + .addHeader("Authorization", basicAuth) + .build(); + } else if (tokenFlowFlag.equals("technicalUser")) { + System.out.println("Technical user token flow"); + request = + new Request.Builder() + .url(authUrl + "/oauth/token?grant_type=client_credentials") + .method("POST", body) + .addHeader("Authorization", basicAuth) + .build(); + } else { + throw new IllegalArgumentException("Invalid token flow specified: " + tokenFlowFlag); + } + + Request requestNoRoles = + new Request.Builder() + .url( + authUrl + + "/oauth/token?grant_type=password&username=" + + noSDMRoleUsername + + "&password=" + + noSDMRoleUserPassword) + .method("POST", body) + .addHeader("Authorization", basicAuth) + .build(); + + Response response = client.newCall(request).execute(); + Response responseNoRoles = client.newCall(requestNoRoles).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); + } + if (responseNoRoles.code() != 200) { + System.out.println("Token generation failed. Response code: " + responseNoRoles.code()); + String errorBody = responseNoRoles.body().string(); + System.out.println("Error body: " + errorBody); + } + token = new ObjectMapper().readTree(response.body().string()).get("access_token").asText(); + tokenNoRoles = + new ObjectMapper().readTree(responseNoRoles.body().string()).get("access_token").asText(); + response.close(); + responseNoRoles.close(); + Map config = new HashMap<>(); + config.put("Authorization", "Bearer " + token); + Map configNoRoles = new HashMap<>(); + configNoRoles.put("Authorization", "Bearer " + tokenNoRoles); + if (tenancyModel.equals("multi")) { + api = new ApiMT(config); + apiNoRoles = new ApiMT(configNoRoles); + } else if (tenancyModel.equals("single")) { + config.put("serviceName", serviceName); + configNoRoles.put("serviceName", serviceName); + api = new Api(config); + apiNoRoles = new Api(configNoRoles); + } else { + throw new IllegalArgumentException("Invalid tenancy model specified: " + tenancyModel); + } + + // Pre-test cleanup: remove any stale test-fixture books (author == "author") from prior runs. + // Failures here are logged but never block the tests so we can still see the real test outcome. + cleanupStaleTestData(client); + } + + /** + * Deletes any leftover {@code Books} rows whose {@code author} field equals the test-fixture + * literal {@code "author"} โ€” both active and draft. Best-effort: any IO/HTTP failure is logged + * with a warning and the method returns so the real tests can still run. + */ + private static void cleanupStaleTestData(OkHttpClient client) { + String tenancyModel = System.getProperty("tenancyModel"); + String baseUrl = + "multi".equals(tenancyModel) + ? "https://" + appUrl + "/api/admin/" + entityName + : "https://" + appUrl + "/odata/v4/" + srvpath + "/" + entityName; + int activeDeleted = + cleanupBookSet(client, baseUrl + "?$filter=author/name eq '" + AUTHOR_NAME + "'", true); + int draftDeleted = + cleanupBookSet( + client, + baseUrl + "?$filter=author/name eq '" + AUTHOR_NAME + "' and IsActiveEntity eq false", + false); + System.out.println( + "๐Ÿงน Pre-test cleanup: deleted " + + activeDeleted + + " active and " + + draftDeleted + + " draft fixture book(s) (author='author')"); + } + + /** + * Fetches all books matching {@code filterUrl} and deletes each by ID. Returns count successfully + * deleted. Never throws โ€” IO failures are swallowed with a warning so cleanup never blocks tests. + */ + private static int cleanupBookSet(OkHttpClient client, String filterUrl, boolean active) { + int deleted = 0; + try { + Request listReq = + new Request.Builder() + .url(filterUrl) + .get() + .addHeader("Authorization", "Bearer " + token) + .build(); + try (Response listRes = client.newCall(listReq).execute()) { + if (listRes.code() != 200) { + System.out.println( + "โš ๏ธ Cleanup list (" + + (active ? "active" : "draft") + + ") returned HTTP " + + listRes.code() + + " โ€” skipping"); + return 0; + } + ObjectMapper mapper = new ObjectMapper(); + com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(listRes.body().string()); + com.fasterxml.jackson.databind.JsonNode values = root.path("value"); + if (!values.isArray()) { + return 0; + } + for (com.fasterxml.jackson.databind.JsonNode book : values) { + String id = book.path("ID").asText(""); + if (id.isEmpty()) { + continue; + } + try { + String resp = + active + ? api.deleteEntity(appUrl, entityName, id) + : api.deleteEntityDraft(appUrl, entityName, id); + if ("Entity Deleted".equals(resp) || "Deleted".equals(resp)) { + deleted++; + } else { + System.out.println( + "โš ๏ธ Cleanup could not delete " + + (active ? "active" : "draft") + + " book " + + id + + " (response: " + + resp + + ")"); + } + } catch (Exception innerEx) { + System.out.println( + "โš ๏ธ Cleanup delete failed for " + + (active ? "active" : "draft") + + " book " + + id + + ": " + + innerEx.getMessage()); + } + } + } + } catch (Exception e) { + System.out.println( + "โš ๏ธ Cleanup " + + (active ? "active" : "draft") + + " list-fetch failed (continuing): " + + e.getMessage()); + } + return deleted; + } + + @Test + @Order(1) + void testCreateActiveEntityAndCreateAttachmentInActiveOnAllFacets() throws IOException { + System.out.println( + "Test (1) : Create entity, activate it, and create attachment on all facets via active" + + " entity flow"); + boolean testStatus = false; + String response = + api.createEntityDraftWithAuthor(appUrl, entityName, entityName2, srvpath, AUTHOR_NAME); + if (!response.equals("Could not create entity")) { + entityID = response; + response = api.saveEntityDraft(appUrl, entityName, srvpath, entityID); + if (response.equals("Saved")) { + int successCount = 0; + for (String f : facet) { + String r = api.createAttachmentInActive(appUrl, entityName, f, entityID); + if (r.equals("Attachment created in active entity")) { + successCount++; + } else { + System.out.println("createAttachmentInActive failed on facet '" + f + "': " + r); + } + } + if (successCount == facet.length) { + testStatus = true; + } + } + } + if (!testStatus) { + fail( + "Could not create attachment via active entity flow on all facets. Last response: " + + response); + } + } + + @Test + @Order(2) + void testVerifyAttachmentsExistOnAllFacetsAfterActiveEntityCreation() throws IOException { + System.out.println( + "Test (2) : Verify attachment appears in metadata on every facet after active entity create"); + boolean testStatus = true; + for (int i = 0; i < facet.length; i++) { + List> attachments = + api.fetchEntityMetadata(appUrl, entityName, facet[i], entityID); + if (attachments == null || attachments.isEmpty()) { + testStatus = false; + System.out.println("No attachments found on facet '" + facet[i] + "'"); + continue; + } + String id = (String) attachments.get(0).get("ID"); + if (id == null || id.isEmpty()) { + testStatus = false; + continue; + } + activeAttachmentIDs[i] = id; + } + if (!testStatus) { + fail( + "Attachment not found in entity metadata on one or more facets after" + + " createAttachmentInActive"); + } + } + + @Test + @Order(3) + void testVerifySDMMetadataOnAllFacets() throws IOException { + System.out.println( + "Test (3) : Verify SDM metadata (objectId, folderId, uploadStatus) on every facet"); + for (int i = 0; i < facet.length; i++) { + Map metadata = + api.fetchMetadata(appUrl, entityName, facet[i], entityID, activeAttachmentIDs[i]); + assertNotNull(metadata, "Metadata must not be null for facet '" + facet[i] + "'"); + Object objectId = metadata.get("objectId"); + assertNotNull( + objectId, "objectId must be populated on facet '" + facet[i] + "' by @After handler"); + assertFalse( + objectId.toString().isEmpty(), "objectId must not be empty on facet '" + facet[i] + "'"); + Object folderId = metadata.get("folderId"); + assertNotNull(folderId, "folderId must be set on facet '" + facet[i] + "'"); + assertFalse( + folderId.toString().isEmpty(), "folderId must not be empty on facet '" + facet[i] + "'"); + Object uploadStatus = metadata.get("uploadStatus"); + assertEquals( + "Success", uploadStatus, "uploadStatus must be Success on facet '" + facet[i] + "'"); + } + } + + @Test + @Order(4) + void testReadContentOfActiveEntityAttachmentsOnAllFacets() throws IOException { + System.out.println( + "Test (4) : Read attachment content on every facet โ€” confirms file is in SDM"); + for (int i = 0; i < facet.length; i++) { + String response = + api.readAttachment(appUrl, entityName, facet[i], entityID, activeAttachmentIDs[i]); + if (!response.equals("OK")) { + fail( + "Could not read attachment content on facet '" + facet[i] + "'. Response: " + response); + } + } + } + + @Test + @Order(5) + void testCreateAttachmentInActiveWithoutSDMRoleOnAllFacets() throws IOException { + System.out.println( + "Test (5) : Create attachment via active entity without SDM role on every facet โ€” expect" + + " 500"); + boolean testStatus = false; + String response = + api.createEntityDraftWithAuthor(appUrl, entityName, entityName2, srvpath, AUTHOR_NAME); + if (!response.equals("Could not create entity")) { + entityID2 = response; + String saveResponse = api.saveEntityDraft(appUrl, entityName, srvpath, entityID2); + if (saveResponse.equals("Saved")) { + int rejectedCount = 0; + for (String f : facet) { + String createResponse = + apiNoRoles.createAttachmentInActive(appUrl, entityName, f, entityID2); + if (createResponse.contains("\"code\":\"500\"") + && createResponse.contains( + "You do not have the required permissions to upload attachments")) { + rejectedCount++; + } else { + System.out.println( + "Unexpected response from no-roles user on facet '" + f + "': " + createResponse); + } + } + if (rejectedCount == facet.length) { + testStatus = true; + } + } + } + if (!testStatus) { + fail("Attachment was created without SDM role on one or more facets, or entity setup failed"); + } + } + + @Test + @Order(6) + void testCreateAttachmentInActiveCoexistsWithDraftUploadsOnAllFacets() throws IOException { + System.out.println( + "Test (6) : Active entity flow coexists with draft-uploaded attachments across all facets"); + boolean testStatus = false; + String response = + api.createEntityDraftWithAuthor(appUrl, entityName, entityName2, srvpath, AUTHOR_NAME); + if (!response.equals("Could not create entity")) { + entityID3 = response; + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource("sample.pdf").getFile()); + int draftUploadCount = 0; + for (String f : facet) { + Map postData = new HashMap<>(); + postData.put("up__ID", entityID3); + postData.put("mimeType", "application/pdf"); + postData.put("createdAt", new Date().toString()); + postData.put("createdBy", "test@test.com"); + postData.put("modifiedBy", "test@test.com"); + List createResponse = + api.createAttachment(appUrl, entityName, f, entityID3, srvpath, postData, file); + if (createResponse.get(0).equals("Attachment created")) { + draftUploadCount++; + } + } + if (draftUploadCount == facet.length) { + response = api.saveEntityDraft(appUrl, entityName, srvpath, entityID3); + if (response.equals("Saved")) { + int activeCount = 0; + for (String f : facet) { + String activeResponse = api.createAttachmentInActive(appUrl, entityName, f, entityID3); + if (activeResponse.equals("Attachment created in active entity")) { + activeCount++; + } + } + if (activeCount == facet.length) { + // Verify every facet now has 2 attachments and both carry an objectId + boolean allValid = true; + for (String f : facet) { + List> attachments = + api.fetchEntityMetadata(appUrl, entityName, f, entityID3); + if (attachments == null || attachments.size() != 2) { + allValid = false; + break; + } + boolean allHaveObjectId = + attachments.stream() + .allMatch( + m -> + m.get("objectId") != null && !m.get("objectId").toString().isEmpty()); + if (!allHaveObjectId) { + allValid = false; + break; + } + } + if (allValid) { + testStatus = true; + } + } + } + } + } + if (!testStatus) { + fail( + "Active entity flow did not coexist properly with draft-uploaded attachments on all" + + " facets"); + } + } + + @Test + @Order(7) + void testDeleteEntityWithActiveEntityAttachmentsOnAllFacets() { + System.out.println("Test (7) : Delete entity that has active-entity attachments on all facets"); + boolean testStatus = false; + String response = api.deleteEntity(appUrl, entityName, entityID); + if (response == "Entity Deleted") { + response = api.checkEntity(appUrl, entityName, entityID); + if (response.equals("Entity doesn't exist")) { + testStatus = true; + } + } + if (!testStatus) { + fail("Could not delete entity with active entity attachments. Response: " + response); + } + } + + @Test + @Order(8) + void testCleanup() { + System.out.println("Test (8) : Cleanup remaining test entities"); + boolean testStatus = true; + if (entityID2 != null && !entityID2.isEmpty()) { + String response = api.deleteEntity(appUrl, entityName, entityID2); + if (response != "Entity Deleted") { + System.out.println("Warning: could not delete entityID2: " + response); + testStatus = false; + } + } + if (entityID3 != null && !entityID3.isEmpty()) { + String response = api.deleteEntity(appUrl, entityName, entityID3); + if (response != "Entity Deleted") { + System.out.println("Warning: could not delete entityID3: " + response); + testStatus = false; + } + } + if (!testStatus) { + fail("Cleanup failed โ€” some test entities could not be deleted"); + } + } +}