diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 755fd3a8d1..21a2e0aef1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -158,3 +158,9 @@ updates: - "8.16" registries: - maven-repository-artifacts-alfresco-com-nexus-content-groups-int +- package-ecosystem: "docker" + directory: "packaging/docker-alfresco/" + schedule: + interval: "daily" + time: "22:00" + timezone: Africa/Abidjan diff --git a/.travis.yml b/.travis.yml index 9e508c8e48..71ba0fa028 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ --- dist: focal language: java -jdk: openjdk11 +jdk: openjdk17 services: - docker @@ -53,7 +53,7 @@ jobs: - name: "Source Clear Scan (SCA)" stage: test - if: branch = master OR branch =~ /release\/.*/ + if: (branch = master OR branch =~ /release\/.*/) AND type != pull_request # Run Veracode install: skip script: travis_wait 30 bash scripts/travis/source_clear.sh @@ -69,8 +69,8 @@ jobs: if: commit_message !~ /\[skip repo\]/ install: skip before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.3 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:14.4 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 - docker run -d -p 8090:8090 -e JAVA_OPTS=" -Xms256m -Xmx256m" alfresco/alfresco-transform-core-aio:${TRANSFORMERS_TAG} script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AppContext01TestSuite -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco @@ -78,16 +78,16 @@ jobs: if: commit_message !~ /\[skip repo\]/ install: skip before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.3 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:14.4 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AppContext02TestSuite -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco - name: "Repository - AppContext03TestSuite" if: commit_message !~ /\[skip repo\]/ install: skip before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.3 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:14.4 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 - docker run -d -p 8090:8090 -e JAVA_OPTS=" -Xms256m -Xmx256m" alfresco/alfresco-transform-core-aio:${TRANSFORMERS_TAG} script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AppContext03TestSuite -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco @@ -95,8 +95,8 @@ jobs: if: commit_message !~ /\[skip repo\]/ install: skip before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.3 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:14.4 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 - docker run -d -p 8090:8090 -e JAVA_OPTS=" -Xms256m -Xmx256m" alfresco/alfresco-transform-core-aio:${TRANSFORMERS_TAG} script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AppContext04TestSuite -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco @@ -104,8 +104,8 @@ jobs: if: commit_message !~ /\[skip repo\]/ install: skip before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.3 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:14.4 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 - mkdir -p "${HOME}/tmp" - cp repository/src/test/resources/realms/alfresco-realm.json "${HOME}/tmp" - export HOST_IP=$(hostname -I | cut -f1 -d' ') @@ -116,8 +116,8 @@ jobs: if: commit_message !~ /\[skip repo\]/ install: skip before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.3 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:14.4 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 - docker run -d -p 8090:8090 -e JAVA_OPTS=" -Xms256m -Xmx256m" alfresco/alfresco-transform-core-aio:${TRANSFORMERS_TAG} script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AppContext06TestSuite -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco @@ -125,8 +125,8 @@ jobs: if: commit_message !~ /\[skip repo\]/ install: skip before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.3 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:14.4 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 - docker run -d -p 8090:8090 -e JAVA_OPTS=" -Xms256m -Xmx256m" alfresco/alfresco-transform-core-aio:${TRANSFORMERS_TAG} script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AppContextExtraTestSuite -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco @@ -134,8 +134,8 @@ jobs: if: commit_message !~ /\[skip repo\]/ install: skip before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.3 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:14.4 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 - docker run -d -p 8090:8090 -e JAVA_OPTS=" -Xms256m -Xmx256m" alfresco/alfresco-transform-core-aio:${TRANSFORMERS_TAG} script: travis_wait 20 mvn -B test -pl repository -am -Dtest=MiscContextTestSuite -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco @@ -143,8 +143,8 @@ jobs: if: commit_message !~ /\[skip repo\]/ install: skip before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.3 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:14.4 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 script: travis_wait 20 mvn -B test -pl repository -am -Dtest=SearchTestSuite -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco -Dindex.subsystem.name=solr6 - name: "Repository - MariaDB 10.2.18 tests" @@ -152,7 +152,7 @@ jobs: install: skip before_script: - docker run -d -p 3307:3306 --name mariadb -e MYSQL_ROOT_PASSWORD=alfresco -e MYSQL_USER=alfresco -e MYSQL_DATABASE=alfresco -e MYSQL_PASSWORD=alfresco mariadb:10.2.18 --transaction-isolation=READ-COMMITTED --max-connections=300 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AllDBTestsTestSuite -DfailIfNoTests=false -Ddb.name=alfresco -Ddb.url=jdbc:mariadb://localhost:3307/alfresco?useUnicode=yes\&characterEncoding=UTF-8 -Ddb.username=alfresco -Ddb.password=alfresco -Ddb.driver=org.mariadb.jdbc.Driver - name: "Repository - MariaDB 10.4 tests" @@ -160,7 +160,7 @@ jobs: install: skip before_script: - docker run -d -p 3307:3306 --name mariadb -e MYSQL_ROOT_PASSWORD=alfresco -e MYSQL_USER=alfresco -e MYSQL_DATABASE=alfresco -e MYSQL_PASSWORD=alfresco mariadb:10.4 --transaction-isolation=READ-COMMITTED --max-connections=300 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AllDBTestsTestSuite -DfailIfNoTests=false -Ddb.name=alfresco -Ddb.url=jdbc:mariadb://localhost:3307/alfresco?useUnicode=yes\&characterEncoding=UTF-8 -Ddb.username=alfresco -Ddb.password=alfresco -Ddb.driver=org.mariadb.jdbc.Driver - name: "Repository - MariaDB 10.5 tests" @@ -168,7 +168,7 @@ jobs: install: skip before_script: - docker run -d -p 3307:3306 --name mariadb -e MYSQL_ROOT_PASSWORD=alfresco -e MYSQL_USER=alfresco -e MYSQL_DATABASE=alfresco -e MYSQL_PASSWORD=alfresco mariadb:10.5 --transaction-isolation=READ-COMMITTED --max-connections=300 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AllDBTestsTestSuite -DfailIfNoTests=false -Ddb.name=alfresco -Ddb.url=jdbc:mariadb://localhost:3307/alfresco?useUnicode=yes\&characterEncoding=UTF-8 -Ddb.username=alfresco -Ddb.password=alfresco -Ddb.driver=org.mariadb.jdbc.Driver - name: "Repository - MariaDB 10.6 tests" @@ -177,7 +177,7 @@ jobs: install: skip before_script: - docker run -d -p 3307:3306 --name mariadb -e MYSQL_ROOT_PASSWORD=alfresco -e MYSQL_USER=alfresco -e MYSQL_DATABASE=alfresco -e MYSQL_PASSWORD=alfresco mariadb:10.6 --transaction-isolation=READ-COMMITTED --max-connections=300 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AllDBTestsTestSuite -DfailIfNoTests=false -Ddb.name=alfresco -Ddb.url=jdbc:mariadb://localhost:3307/alfresco?useUnicode=yes\&characterEncoding=UTF-8 -Ddb.username=alfresco -Ddb.password=alfresco -Ddb.driver=org.mariadb.jdbc.Driver - name: "Repository - MySQL 8 tests" @@ -186,88 +186,47 @@ jobs: install: skip before_script: - docker run -d -p 3307:3306 -e MYSQL_ROOT_PASSWORD=alfresco -e MYSQL_USER=alfresco -e MYSQL_DATABASE=alfresco -e MYSQL_PASSWORD=alfresco mysql:8 --transaction-isolation='READ-COMMITTED' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AllDBTestsTestSuite -DfailIfNoTests=false -Ddb.driver=com.mysql.jdbc.Driver -Ddb.name=alfresco -Ddb.url=jdbc:mysql://localhost:3307/alfresco -Ddb.username=alfresco -Ddb.password=alfresco - - name: "Repository - PostgreSQL 10.9 tests" + - name: "Repository - PostgreSQL 13.7 tests" if: (branch =~ /(release\/.*$|master)/ AND commit_message !~ /\[skip db\]/ AND type != pull_request) OR commit_message =~ /\[db\]/ install: skip before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:10.9 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.7 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AllDBTestsTestSuite -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco - - name: "Repository - PostgreSQL 11.7 tests" - if: (branch =~ /(release\/.*$|master)/ AND commit_message !~ /\[skip db\]/ AND type != pull_request) OR commit_message =~ /\[db\]/ - install: skip - before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:11.7 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 - script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AllDBTestsTestSuite -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco - - - name: "Repository - PostgreSQL 11.12 tests" - if: (branch =~ /(release\/.*$|master)/ AND commit_message !~ /\[skip db\]/ AND type != pull_request) OR commit_message =~ /\[db\]/ - install: skip - before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:11.12 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 - script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AllDBTestsTestSuite -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco - - - name: "Repository - PostgreSQL 12.4 tests" - if: (branch =~ /(release\/.*$|master)/ AND commit_message !~ /\[skip db\]/ AND type != pull_request) OR commit_message =~ /\[db\]/ - install: skip - before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:12.4 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 - script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AllDBTestsTestSuite -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco - - - name: "Repository - PostgreSQL 12.7 tests" - if: (branch =~ /(release\/.*$|master)/ AND commit_message !~ /\[skip db\]/ AND type != pull_request) OR commit_message =~ /\[db\]/ - install: skip - before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:12.7 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 - script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AllDBTestsTestSuite -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco - - - name: "Repository - PostgreSQL 13.1 tests" + - name: "Repository - PostgreSQL 14.4 tests" # We only run DB tests on the latest version of PostgreSQL on feature branches - if: (branch =~ /(release\/.*$|master)/ AND commit_message !~ /\[skip db\]/ AND type != pull_request) OR commit_message =~ /\[db\]/ + if: commit_message !~ /\[skip db\]/ OR commit_message =~ /\[db\]/ OR commit_message =~ /\[latest db\]/ install: skip before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.1 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 - script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AllDBTestsTestSuite -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco - - - name: "Repository - PostgreSQL 13.3 tests" - # We only run DB tests on the latest version of PostgreSQL on feature branches - if: commit_message !~ /\[skip db\]/ OR commit_message =~ /\[latest db\]/ - install: skip - before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.3 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:14.4 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 script: travis_wait 20 mvn -B test -pl repository -am -Dtest=AllDBTestsTestSuite -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco - name: "Repository - Messaging tests" if: commit_message !~ /\[skip repo\]/ install: skip before_script: - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 script: travis_wait 20 mvn -B test -pl repository -am -Dtest=CamelRoutesTest,CamelComponentsTest -DfailIfNoTests=false - name: "Remote-api - AppContext01TestSuite" if: commit_message !~ /\[skip repo\]/ install: travis_retry travis_wait 40 env REQUIRES_INSTALLED_ARTIFACTS=true bash scripts/travis/build.sh before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.3 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:14.4 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 script: travis_wait 20 mvn -B test -pl remote-api -Dtest=AppContext01TestSuite -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco - name: "Remote-api - AppContext02TestSuite" if: commit_message !~ /\[skip repo\]/ install: travis_retry travis_wait 40 env REQUIRES_INSTALLED_ARTIFACTS=true bash scripts/travis/build.sh before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.3 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:14.4 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 - docker run -d -p 8090:8090 -e JAVA_OPTS=" -Xms256m -Xmx256m" alfresco/alfresco-transform-core-aio:${TRANSFORMERS_TAG} script: travis_wait 20 mvn -B test -pl remote-api -Dtest=AppContext02TestSuite -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco @@ -275,8 +234,8 @@ jobs: if: commit_message !~ /\[skip repo\]/ install: travis_retry travis_wait 40 env REQUIRES_INSTALLED_ARTIFACTS=true bash scripts/travis/build.sh before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.3 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:14.4 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 - docker run -d -p 8090:8090 -e JAVA_OPTS=" -Xms256m -Xmx256m" alfresco/alfresco-transform-core-aio:${TRANSFORMERS_TAG} script: travis_wait 20 mvn -B test -pl remote-api -Dtest=AppContext03TestSuite -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco @@ -284,8 +243,8 @@ jobs: if: commit_message !~ /\[skip repo\]/ install: travis_retry travis_wait 40 env REQUIRES_INSTALLED_ARTIFACTS=true bash scripts/travis/build.sh before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.3 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:14.4 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 - docker run -d -p 8090:8090 -e JAVA_OPTS=" -Xms256m -Xmx256m" alfresco/alfresco-transform-core-aio:${TRANSFORMERS_TAG} script: travis_wait 20 mvn -B test -pl remote-api -Dtest=AppContext04TestSuite -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco @@ -293,8 +252,8 @@ jobs: if: commit_message !~ /\[skip repo\]/ install: travis_retry travis_wait 40 env REQUIRES_INSTALLED_ARTIFACTS=true bash scripts/travis/build.sh before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.3 postgres -c 'max_connections=300' - - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.16.1 + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:14.4 postgres -c 'max_connections=300' + - docker run -d -p 61616:61616 -p 5672:5672 alfresco/alfresco-activemq:5.17.1-jre11-rockylinux8 script: travis_wait 20 mvn -B test -pl remote-api -Dtest=AppContextExtraTestSuite -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco - name: "REST API TAS tests part1" @@ -305,6 +264,7 @@ jobs: - ${TAS_SCRIPTS}/start-compose.sh ${TAS_ENVIRONMENT}/docker-compose-minimal+transforms.yml - ${TAS_SCRIPTS}/wait-for-alfresco-start.sh "http://localhost:8082/alfresco" script: travis_wait 60 mvn -B verify -f packaging/tests/tas-restapi/pom.xml -Pall-tas-tests,run-restapi-part1 -Denvironment=default -DrunBugs=false + after_failure: ${TAS_SCRIPTS}/output_logs_for_failures.sh "packaging/tests/tas-restapi" - name: "REST API TAS tests part2" if: (branch =~ /(release\/.*$|master)/ AND commit_message !~ /\[skip tas\]/) OR commit_message =~ /\[tas\]/ @@ -313,6 +273,7 @@ jobs: - ${TAS_SCRIPTS}/start-compose.sh ${TAS_ENVIRONMENT}/docker-compose-minimal+transforms.yml - ${TAS_SCRIPTS}/wait-for-alfresco-start.sh "http://localhost:8082/alfresco" script: travis_wait 60 mvn -B verify -f packaging/tests/tas-restapi/pom.xml -Pall-tas-tests,run-restapi-part2 -Denvironment=default -DrunBugs=false + after_failure: ${TAS_SCRIPTS}/output_logs_for_failures.sh "packaging/tests/tas-restapi" - name: "REST API TAS tests part3" if: (branch =~ /(release\/.*$|master)/ AND commit_message !~ /\[skip tas\]/) OR commit_message =~ /\[tas\]/ @@ -321,6 +282,7 @@ jobs: - ${TAS_SCRIPTS}/start-compose.sh ${TAS_ENVIRONMENT}/docker-compose-minimal+transforms.yml - ${TAS_SCRIPTS}/wait-for-alfresco-start.sh "http://localhost:8082/alfresco" script: travis_wait 60 mvn -B verify -f packaging/tests/tas-restapi/pom.xml -Pall-tas-tests,run-restapi-part3 -Denvironment=default -DrunBugs=false + after_failure: ${TAS_SCRIPTS}/output_logs_for_failures.sh "packaging/tests/tas-restapi" - name: "CMIS TAS tests - BROWSER binding" if: (branch =~ /(release\/.*$|master)/ AND commit_message !~ /\[skip tas\]/) OR commit_message =~ /\[tas\]/ @@ -329,6 +291,7 @@ jobs: - ${TAS_SCRIPTS}/start-compose.sh ${TAS_ENVIRONMENT}/docker-compose-minimal+transforms.yml - ${TAS_SCRIPTS}/wait-for-alfresco-start.sh "http://localhost:8082/alfresco" script: travis_wait 40 mvn -B verify -f packaging/tests/tas-cmis/pom.xml -Pall-tas-tests,run-cmis-browser -Denvironment=default -DrunBugs=false + after_failure: ${TAS_SCRIPTS}/output_logs_for_failures.sh "packaging/tests/tas-cmis" - name: "CMIS TAS tests - ATOM binding" if: (branch =~ /(release\/.*$|master)/ AND commit_message !~ /\[skip tas\]/) OR commit_message =~ /\[tas\]/ @@ -337,6 +300,7 @@ jobs: - ${TAS_SCRIPTS}/start-compose.sh ${TAS_ENVIRONMENT}/docker-compose-minimal+transforms.yml - ${TAS_SCRIPTS}/wait-for-alfresco-start.sh "http://localhost:8082/alfresco" script: travis_wait 40 mvn -B verify -f packaging/tests/tas-cmis/pom.xml -Pall-tas-tests,run-cmis-atom -Denvironment=default -DrunBugs=false + after_failure: ${TAS_SCRIPTS}/output_logs_for_failures.sh "packaging/tests/tas-cmis" - name: "CMIS TAS tests - WEBSERVICES binding" if: (branch =~ /(release\/.*$|master)/ AND commit_message !~ /\[skip tas\]/) OR commit_message =~ /\[tas\]/ @@ -345,6 +309,7 @@ jobs: - ${TAS_SCRIPTS}/start-compose.sh ${TAS_ENVIRONMENT}/docker-compose-minimal+transforms.yml - ${TAS_SCRIPTS}/wait-for-alfresco-start.sh "http://localhost:8082/alfresco" script: travis_wait 40 mvn -B verify -f packaging/tests/tas-cmis/pom.xml -Pall-tas-tests,run-cmis-webservices -Denvironment=default -DrunBugs=false + after_failure: ${TAS_SCRIPTS}/output_logs_for_failures.sh "packaging/tests/tas-cmis" - name: "Email TAS tests" if: (branch =~ /(release\/.*$|master)/ AND commit_message !~ /\[skip tas\]/) OR commit_message =~ /\[tas\]/ @@ -353,6 +318,7 @@ jobs: - ${TAS_SCRIPTS}/start-compose.sh ${TAS_ENVIRONMENT}/docker-compose-minimal.yml - ${TAS_SCRIPTS}/wait-for-alfresco-start.sh "http://localhost:8082/alfresco" script: travis_wait 30 mvn -B verify -f packaging/tests/tas-email/pom.xml -Pall-tas-tests -Denvironment=default -DrunBugs=false + after_failure: ${TAS_SCRIPTS}/output_logs_for_failures.sh "packaging/tests/tas-email" - name: "WebDAV TAS tests" if: (branch =~ /(release\/.*$|master)/ AND commit_message !~ /\[skip tas\]/) OR commit_message =~ /\[tas\]/ @@ -361,6 +327,7 @@ jobs: - ${TAS_SCRIPTS}/start-compose.sh ${TAS_ENVIRONMENT}/docker-compose-minimal.yml - ${TAS_SCRIPTS}/wait-for-alfresco-start.sh "http://localhost:8082/alfresco" script: travis_wait 30 mvn -B verify -f packaging/tests/tas-webdav/pom.xml -Pall-tas-tests -Denvironment=default -DrunBugs=false + after_failure: ${TAS_SCRIPTS}/output_logs_for_failures.sh "packaging/tests/tas-webdav" - name: "Integration TAS tests" if: (branch =~ /(release\/.*$|master)/ AND commit_message !~ /\[skip tas\]/) OR commit_message =~ /\[tas\]/ @@ -368,13 +335,15 @@ jobs: before_script: - ${TAS_SCRIPTS}/start-compose.sh ${TAS_ENVIRONMENT}/docker-compose-minimal.yml - ${TAS_SCRIPTS}/wait-for-alfresco-start.sh "http://localhost:8082/alfresco" + - travis_retry travis_wait 40 mvn install -pl :alfresco-community-repo-integration-test -am -DskipTests -Pall-tas-tests script: travis_wait 30 mvn -B verify -f packaging/tests/tas-integration/pom.xml -Pall-tas-tests -Denvironment=default -DrunBugs=false + after_failure: ${TAS_SCRIPTS}/output_logs_for_failures.sh "packaging/tests/tas-integration" - name: "Share Services - ShareServicesTestSuite" if: commit_message !~ /\[skip repo\]/ install: skip before_script: - - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:13.3 postgres -c 'max_connections=300' + - docker run -d -p 5433:5432 -e POSTGRES_PASSWORD=alfresco -e POSTGRES_USER=alfresco -e POSTGRES_DB=alfresco postgres:14.4 postgres -c 'max_connections=300' script: travis_wait 20 mvn -B test -pl :alfresco-share-services -am -Dtest=ShareServicesTestSuite -DfailIfNoTests=false -Ddb.driver=org.postgresql.Driver -Ddb.name=alfresco -Ddb.url=jdbc:postgresql://localhost:5433/alfresco -Ddb.username=alfresco -Ddb.password=alfresco - name: "AGS Unit & Integration Tests 01 (PostgreSQL)" diff --git a/amps/ags/pom.xml b/amps/ags/pom.xml index 0f444eef6d..a5b6714d7b 100644 --- a/amps/ags/pom.xml +++ b/amps/ags/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo-amps - 17.43-SNAPSHOT + 20.5-SNAPSHOT @@ -30,16 +30,20 @@ maven-surefire-plugin + --illegal-access=permit + --add-opens=java.base/java.lang=ALL-UNNAMED maven-failsafe-plugin + --illegal-access=permit + --add-opens=java.base/java.lang=ALL-UNNAMED diff --git a/amps/ags/rm-automation/pom.xml b/amps/ags/rm-automation/pom.xml index 4bdf70134f..8e6a0f17ab 100644 --- a/amps/ags/rm-automation/pom.xml +++ b/amps/ags/rm-automation/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-governance-services-community-parent - 17.43-SNAPSHOT + 20.5-SNAPSHOT diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/pom.xml b/amps/ags/rm-automation/rm-automation-community-rest-api/pom.xml index 09128c4e92..12bdc84810 100644 --- a/amps/ags/rm-automation/rm-automation-community-rest-api/pom.xml +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-governance-services-automation-community-repo - 17.43-SNAPSHOT + 20.5-SNAPSHOT @@ -45,7 +45,7 @@ org.slf4j slf4j-reload4j - 1.7.35 + ${dependency.slf4j.version} test diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/LinksAPI.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/LinksAPI.java new file mode 100644 index 0000000000..0eb10ff546 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/LinksAPI.java @@ -0,0 +1,73 @@ +/*- + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.v0; + +import org.alfresco.rest.core.v0.BaseAPI; +import org.apache.http.HttpResponse; +import org.json.JSONArray; +import org.json.JSONObject; +import org.springframework.stereotype.Component; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.text.MessageFormat; +import java.util.List; + +/** + * Methods to make API requests using v0 API for Linking Records + * + * @author Kavit Shah + * @since 3.2 + */ +@Component +public class LinksAPI extends BaseAPI { + + private static final String LINK_API = "{0}doclib/action/rm-link/site/rm/documentLibrary/{1}"; + + /** + * Creates the Link + * + * @param user The username of the user to use. + * @param password The password of the user. + * @param expectedStatusCode The expected return status code. + * @param sourcePath The Source of link the record. This should be in the format + * "{site}/{container}/{path}", "{site}/{container}", "{store_type}/{store_id}/{id}/{path}", + * "{store_type}/{store_id}/{id}" or "{store_type}/{store_id}". + * @param nodeRefs The Node that needs to be linked. + * @return The HTTP Response. + * @throws AssertionError If the API didn't return the expected status code. + */ + public HttpResponse linkRecord(String user, String password, int expectedStatusCode, String sourcePath, List nodeRefs) throws UnsupportedEncodingException { + JSONObject requestParams = new JSONObject(); + requestParams.put("nodeRefs", new JSONArray(nodeRefs)); + + return doSlingshotPostJsonRequest(user, password, expectedStatusCode, requestParams, + MessageFormat.format(LINK_API, "{0}", sourcePath)); + } + +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RMAuditAPI.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RMAuditAPI.java index f63a6efb50..6844a829e0 100644 --- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RMAuditAPI.java +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RMAuditAPI.java @@ -26,6 +26,7 @@ */ package org.alfresco.rest.v0; +import static org.apache.http.HttpStatus.SC_OK; import static org.testng.Assert.assertTrue; import java.io.UnsupportedEncodingException; @@ -36,6 +37,7 @@ import java.util.List; import org.alfresco.rest.core.v0.BaseAPI; import org.alfresco.rest.rm.community.model.audit.AuditEntry; import org.alfresco.rest.rm.community.util.PojoUtility; +import org.apache.http.HttpResponse; import org.json.JSONArray; import org.json.JSONObject; import org.slf4j.Logger; @@ -58,6 +60,8 @@ public class RMAuditAPI extends BaseAPI private static final String RM_AUDIT_API = "{0}rma/admin/rmauditlog"; private static final String RM_AUDIT_LOG_API = RM_AUDIT_API + "?{1}"; + private static final String RM_AUDIT_LOG_AS_RECORD = "{0}node/{1}/rmauditlog"; + /** * Returns a list of rm audit entries . * @@ -84,6 +88,21 @@ public class RMAuditAPI extends BaseAPI return PojoUtility.jsonToObject(auditEntries, AuditEntry.class); } + /** + * Returns a list of rm audit entries . + * + * @param user The username of the user to use. + * @param password The password of the user. + * @param size Maximum number of log entries to return + * @return return All return log entries + */ + public List getRMAuditLogAll(String user, String password, final int size) { + String parameters = "size=" + size; + JSONArray auditEntries = doGetRequest(user, password, + MessageFormat.format(RM_AUDIT_LOG_API,"{0}", parameters)).getJSONObject("data").getJSONArray("entries"); + return PojoUtility.jsonToObject(auditEntries, AuditEntry.class); + } + /** * Clear the list of audit entries. * @@ -100,5 +119,19 @@ public class RMAuditAPI extends BaseAPI && getRMAuditLog(username, password, 100, null).size() == 2); } + /** + * Logs the Audit Log as Record. + * + * @param username The username of the user to use. + * @param password The password of the user. + * @param recNodeRef The Record Node reference for which Audit log should be created as record + * @param destinationNodeRef The Folder id Node reference where the html file should be placed + * @throws AssertionError If the API call didn't create the Audit Log as Record. + */ + public HttpResponse logsAuditLogAsRecord(String username, String password, String recNodeRef, String destinationNodeRef) { + JSONObject requestParams = new JSONObject(); + requestParams.put("destination", destinationNodeRef); + return doPostJsonRequest(username, password, SC_OK, requestParams, RM_AUDIT_LOG_AS_RECORD,recNodeRef); + } } diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RecordCategoriesAPI.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RecordCategoriesAPI.java index 0233e99781..17fee78f10 100644 --- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RecordCategoriesAPI.java +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RecordCategoriesAPI.java @@ -52,7 +52,7 @@ public class RecordCategoriesAPI extends BaseAPI private static final String RM_ACTIONS_API = "{0}rma/actions/ExecutionQueue"; private static final String DISPOSITION_ACTIONS_API = "{0}node/{1}/dispositionschedule/dispositionactiondefinitions"; private static final String DISPOSITION_SCHEDULE_API = "{0}node/{1}/dispositionschedule"; - + private static final String NEXT_DISPOSITION_ACTIONS_API = "{0}node/{1}/nextdispositionaction"; /** * Creates a retention schedule for the category given as parameter @@ -191,4 +191,19 @@ public class RecordCategoriesAPI extends BaseAPI retentionProperties.put(RETENTION_SCHEDULE.RETENTION_INSTRUCTIONS, instructions); return retentionProperties; } + + /** + * Get the Next Disposition Action + * + * @param user + * @param password + * @param recordId + * @return the next disposition schedule action + */ + public JSONObject getNextDispositionAction(String user, String password, String recordId) + { + String nodeRef = NODE_PREFIX + recordId; + JSONObject nextDispositionAction = doGetRequest(user, password, MessageFormat.format(NEXT_DISPOSITION_ACTIONS_API, "{0}", nodeRef)); + return nextDispositionAction; + } } diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RecordFoldersAPI.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RecordFoldersAPI.java index 04b321664b..25f49971b5 100644 --- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RecordFoldersAPI.java +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RecordFoldersAPI.java @@ -74,4 +74,39 @@ public class RecordFoldersAPI extends BaseAPI return null; } + public HttpResponse postFolderAction(String user, String password, JSONObject requestParams, String recordFolder) { + String recNodeRef = getNodeRefSpacesStore() + contentService.getNodeRef(user, password, RM_SITE_ID, recordFolder); + try { + requestParams.put("nodeRef", recNodeRef); + return doPostJsonRequest(user, password, SC_OK, requestParams, RM_ACTIONS_API); + } + catch (Exception error) { + LOGGER.error("Unable to extract response parameter", error); + } + return null; + } + + public HttpResponse postRecordAction(String user, String password, JSONObject requestParams, String recordId) { + try { + requestParams.put("nodeRef", recordId); + return doPostJsonRequest(user, password, SC_OK, requestParams, RM_ACTIONS_API); + } + catch (JSONException error) { + LOGGER.error("Unable to extract response parameter", error); + } + return null; + } + + + public HttpResponse reOpenRecordFolder(String user, String password, String recordFolder) + { + String recNodeRef = getNodeRefSpacesStore() + contentService.getNodeRef(user, password, RM_SITE_ID, recordFolder); + + JSONObject requestParams = new JSONObject(); + requestParams.put("name", "openRecordFolder"); + requestParams.put("nodeRef", recNodeRef); + + return doPostJsonRequest(user, password, SC_OK, requestParams, RM_ACTIONS_API); + } + } diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RecordsAPI.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RecordsAPI.java index 037476d52f..f06dfabc61 100644 --- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RecordsAPI.java +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/main/java/org/alfresco/rest/v0/RecordsAPI.java @@ -360,4 +360,25 @@ public class RecordsAPI extends BaseAPI { return getNodeRefSpacesStore() + getItemNodeRef(username, password, recordPath + "/" + recordName); } + + /** + * Reopens the record given as parameter + * + * @param user the user declaring the document as record + * @param password the user's password + * @param recordName the record name + * @return The HTTP Response. + */ + + public HttpResponse reOpenRecord(String user, String password, String recordName) + { + String recNodeRef = getNodeRefSpacesStore() + contentService.getNodeRef(user, password, RM_SITE_ID, recordName); + + JSONObject requestParams = new JSONObject(); + requestParams.put("name", "undeclareRecord"); + requestParams.put("nodeRef", recNodeRef); + + return doPostJsonRequest(user, password, SC_OK, requestParams, RM_ACTIONS_API); + } + } diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/audit/AuditHoldsTest.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/audit/AuditHoldsTest.java new file mode 100644 index 0000000000..dd1e7c4ad2 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/audit/AuditHoldsTest.java @@ -0,0 +1,146 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rm.community.audit; +import static java.util.Arrays.asList; +import static org.alfresco.rest.rm.community.base.TestData.*; +import static org.alfresco.rest.rm.community.model.audit.AuditEvents.ADD_TO_HOLD; +import static org.alfresco.rest.rm.community.model.audit.AuditEvents.REMOVE_FROM_HOLD; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.utility.data.RandomData.getRandomName; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.core.IsNot.not; +import static org.springframework.http.HttpStatus.CREATED; +import static org.testng.AssertJUnit.*; + +import java.util.Collections; +import java.util.List; +import org.alfresco.dataprep.CMISUtil; +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.audit.AuditEntry; +import org.alfresco.rest.rm.community.model.audit.AuditEvents; +import org.alfresco.rest.rm.community.model.record.Record; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.rm.community.model.recordfolder.RecordFolder; +import org.alfresco.rest.rm.community.model.user.UserRoles; +import org.alfresco.rest.v0.HoldsAPI; +import org.alfresco.rest.v0.service.RMAuditService; +import org.alfresco.rest.v0.service.RoleService; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.UserModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +public class AuditHoldsTest extends BaseRMRestTest { + private final String PREFIX = generateTestPrefix(AuditAddToHoldTests.class); + private final String HOLD1 = PREFIX + "hold1"; + private SiteModel publicSite; + private FileModel testFile; + @Autowired + private RMAuditService rmAuditService; + @Autowired + private HoldsAPI holdsAPI; + @Autowired + private RoleService roleService; + private UserModel rmAdmin; + private RecordCategory recordCategory; + private RecordCategoryChild recordFolder1,recordFolder2; + private List auditEntries; + private String hold1NodeRef; + public static final String RECORD_FOLDER_THREE = "record-folder-three"; + @BeforeClass(alwaysRun = true) + public void preconditionForAuditAddToHoldTests() + { + createRMSiteIfNotExists(); + rmAdmin = roleService.createUserWithRMRole(UserRoles.ROLE_RM_ADMIN.roleId); + + STEP("Create a hold"); + hold1NodeRef = holdsAPI.createHoldAndGetNodeRef(rmAdmin.getUsername(), rmAdmin.getPassword(), HOLD1, HOLD_REASON, + HOLD_DESCRIPTION); + + STEP("Create a collaboration site with a test file."); + publicSite = dataSite.usingAdmin().createPublicRandomSite(); + testFile = dataContent.usingAdmin().usingSite(publicSite).createContent(CMISUtil.DocumentType.TEXT_PLAIN); + + STEP("Create a record category with 2 folders and 1 record"); + recordCategory = createRootCategory(getRandomName("recordCategory")); + recordFolder1 = createRecordFolder(recordCategory.getId(), PREFIX + "recFolder1"); + recordFolder2 = createRecordFolder(recordCategory.getId(), PREFIX + "recFolder2"); + Record recordToBeAdded = createElectronicRecord(recordFolder1.getId(), PREFIX + "record"); + assertStatusCode(CREATED); + + STEP("Add some items to the hold, then remove them from the hold"); + final List itemsList = asList(testFile.getNodeRefWithoutVersion(), recordToBeAdded.getId(), recordFolder2.getId()); + final List holdsList = Collections.singletonList(HOLD1); + holdsAPI.addItemToHold(rmAdmin.getUsername(), rmAdmin.getPassword(), recordToBeAdded.getId(), HOLD1); + holdsAPI.removeItemsFromHolds(rmAdmin.getUsername(), rmAdmin.getPassword(), itemsList, holdsList); + + STEP("Delete the record folder that was held"); + getRestAPIFactory().getRecordFolderAPI().deleteRecordFolder(recordFolder2.getId()); + + STEP("Rename the parent of the record that was held"); + RecordFolder recordFolder = RecordFolder.builder().name(RECORD_FOLDER_THREE).build(); + getRestAPIFactory().getRecordFolderAPI().updateRecordFolder(recordFolder, recordFolder1.getId()); + } + /** + * Data provider with hold events that have links to held items + * + * @return the hold events + */ + @DataProvider (name = "holdsEvents") + public Object[][] getHoldEvents() + { + return new AuditEvents[][] + { + { ADD_TO_HOLD }, + { REMOVE_FROM_HOLD } + }; + } + @Test (dataProvider = "holdsEvents") + public void checkItemPathLink(AuditEvents event) { + auditEntries = rmAuditService.getAuditEntriesFilteredByEvent(getAdminUser(), event); + assertFalse("Audit results should not be empty",auditEntries.size()==0); + final String auditedEvent = event + " - " + testFile.getName(); + assertTrue("Audit results should contain one " + auditedEvent + " event",auditEntries.stream().anyMatch(e -> e.getEvent().startsWith(event.eventDisplayName))); + STEP("Check the audit log contains only an entry for add to hold."); + assertThat(auditEntries, is(not(empty()))); + } + @AfterClass(alwaysRun = true) + private void cleanup() { + dataSite.usingAdmin().deleteSite(publicSite); + deleteRecordFolder(recordFolder1.getId()); + deleteRecordFolder(recordFolder2.getId()); + deleteRecordCategory(recordCategory.getId()); + rmAuditService.clearAuditLog(); + } +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/audit/ElectronicRecordAuditLogTest.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/audit/ElectronicRecordAuditLogTest.java new file mode 100644 index 0000000000..a868c188c8 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/audit/ElectronicRecordAuditLogTest.java @@ -0,0 +1,244 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rm.community.audit; + +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.audit.AuditEntry; +import org.alfresco.rest.rm.community.model.record.Record; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.v0.RMAuditAPI; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.rest.v0.RecordsAPI; +import org.alfresco.test.AlfrescoTest; +import org.alfresco.utility.Utility; +import org.alfresco.utility.model.UserModel; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.util.EntityUtils; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.AssertJUnit; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAspects.ASPECTS_COMPLETED_RECORD; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.createRecordModel; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.test.util.AssertionErrors.assertTrue; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.fail; + +public class ElectronicRecordAuditLogTest extends BaseRMRestTest { + + private Optional rmAdmin; + @Autowired + private RMRolesAndActionsAPI rmRolesAndActionsAPI; + @Autowired + private RMAuditAPI auditLog; + @Autowired + private RecordsAPI recordApi; + /* electronic record details */ + private static final String AUDIT_ELECTRONIC_RECORD = generateTestPrefix(ElectronicRecordAuditLogTest.class) + "electronic record"; + private static final String AUDIT_COMPLETE_REOPEN_ELECTRONIC_RECORD = "Complete Reopen Electronic Record"; + public static final String TITLE = "Title"; + public static final String DESCRIPTION = "Description"; + private RecordCategory category1; + private RecordCategoryChild recordFolder1; + private Record electronicRecord, electronicRecord2; + + @BeforeClass(alwaysRun = true) + public void electronicRecordsAuditLogSetup() + { + createRMSiteIfNotExists(); + rmAdmin = Optional.ofNullable(getDataUser().createRandomTestUser()); + rmRolesAndActionsAPI.assignRoleToUser( + getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), + rmAdmin.get().getUsername(), + "Administrator"); + auditLog.clearAuditLog(rmAdmin.get().getUsername(),rmAdmin.get().getPassword()); + category1 = createRootCategory(TITLE, DESCRIPTION); + recordFolder1 = createFolder(category1.getId(),TITLE); + + electronicRecord = createElectronicRecord(recordFolder1.getId(),AUDIT_ELECTRONIC_RECORD,rmAdmin.get()); + } + + @Test(description = "Audit log for newly filed electronic record") + @AlfrescoTest(jira="RM-4303") + public void newElectronicRecordAudit() { + List auditEntries= auditLog.getRMAuditLogAll(getAdminUser().getUsername(),getAdminUser().getPassword(),100); + + // newly created record contains 2 events: "file to" and metadata update + // the order in which object creation and metadata update are listed isn't always identical due to + // both happening in the same transaction + assertTrue("File To Event is not present.",auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("File to"))); + assertTrue("Updated metadata Event is not present.",auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Updated Metadata"))); + } + + @Test + ( + dependsOnMethods = "newElectronicRecordAudit", + description = "Viewing electronic record audit log is itself an auditable event" + ) + @AlfrescoTest(jira="RM-4303") + public void electronicRecordAuditIsEvent() + { + List auditEntries= auditLog.getRMAuditLogAll(getAdminUser().getUsername(),getAdminUser().getPassword(),100); + assertTrue("Audit View Event is not present.",auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Audit View"))); + } + + @Test + ( + dependsOnMethods = "electronicRecordAuditIsEvent", + description = "Rename electronic record is an edit metadata event" + ) + @AlfrescoTest(jira="RM-4303") + public void renameElectronicRecord() { + auditLog.clearAuditLog(rmAdmin.get().getUsername(),rmAdmin.get().getPassword()); + Record renameElectronicRecord = createRecordModel("edited " + electronicRecord.getName(), "", ""); + + // rename record + getRestAPIFactory().getRecordsAPI().updateRecord(renameElectronicRecord, electronicRecord.getId()); + assertStatusCode(OK); + + // we expect 1 new event: "metadata update" + List auditEntries= auditLog.getRMAuditLogAll(getAdminUser().getUsername(),getAdminUser().getPassword(),100); + assertTrue("Updated metadata Event is not present.",auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Updated Metadata"))); + } + + @Test ( + dependsOnMethods = "newElectronicRecordAudit", + description = "Complete and reopen electronic record") + @AlfrescoTest(jira="RM-4303") + public void completeAndReopenElectronicRecord() { + electronicRecord2 = createElectronicRecord(recordFolder1.getId(),AUDIT_COMPLETE_REOPEN_ELECTRONIC_RECORD); + + // complete record + recordApi.completeRecord(rmAdmin.get().getUsername(),rmAdmin.get().getPassword(), + electronicRecord2.getName()); + + try + { + Utility.sleep(1000, 30000, () -> + { + org.alfresco.rest.rm.community.requests.gscore.api.RecordsAPI recordsAPI = getRestAPIFactory().getRecordsAPI(); + List aspects = recordsAPI.getRecord(electronicRecord2.getId()).getAspectNames(); + // a record must be completed + assertTrue("Record is not completed.",aspects.contains(ASPECTS_COMPLETED_RECORD)); + }); + } + catch (InterruptedException e) + { + fail("InterruptedException received while waiting for results."); + } + + List auditEntries= auditLog.getRMAuditLogAll(getAdminUser().getUsername(),getAdminUser().getPassword(),100); + assertTrue("Complete Record Event is not present.",auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Complete Record"))); + + // Reopen record + recordApi.reOpenRecord(rmAdmin.get().getUsername(),rmAdmin.get().getPassword(), + electronicRecord2.getName()); + + try + { + Utility.sleep(1000, 30000, () -> + { + org.alfresco.rest.rm.community.requests.gscore.api.RecordsAPI recordsAPI = getRestAPIFactory().getRecordsAPI(); + List aspects = recordsAPI.getRecord(electronicRecord2.getId()).getAspectNames(); + // a record mustn't be completed + assertFalse(aspects.contains(ASPECTS_COMPLETED_RECORD)); + }); + } + catch (InterruptedException e) + { + fail("InterruptedException received while waiting for results."); + } + + auditEntries= auditLog.getRMAuditLogAll(getAdminUser().getUsername(),getAdminUser().getPassword(),100); + assertTrue("Reopen Record Event is not present.",auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Reopen Record"))); + } + + @Test + ( + dependsOnMethods = "completeAndReopenElectronicRecord", + description = "File electronic record's audit log as record" + ) + @AlfrescoTest(jira="RM-4303") + public void fileElectronicRecordAuditLogAsRecord() + { + // audit log is stored in the same folder, refresh it so that it appears in the list + HttpResponse auditRecordHttpResponse = auditLog.logsAuditLogAsRecord(rmAdmin.get().getUsername(),rmAdmin.get().getPassword(), + getRecordNodeRef(electronicRecord2.getId()),getFolderNodeRef(recordFolder1.getId())); + JSONObject auditRecordProperties = getAuditPropertyValues(auditRecordHttpResponse); + Record auditRecord = getRestAPIFactory().getRecordsAPI().getRecord(auditRecordProperties.get("record").toString() + .replace("workspace://SpacesStore/","")); + // check audit log + AssertJUnit.assertTrue(auditRecordProperties.get("recordName").toString().endsWith(".html")); + AssertJUnit.assertTrue(auditRecord.getAspectNames().stream().noneMatch(x -> x.startsWith(ASPECTS_COMPLETED_RECORD))); + } + + private String getFolderNodeRef(String folderId) { + return "workspace://SpacesStore/" + folderId; + } + + private String getRecordNodeRef(String recordId) { + return "workspace/SpacesStore/" + recordId; + } + + private JSONObject getAuditPropertyValues(HttpResponse httpResponse) { + HttpEntity entity = httpResponse.getEntity(); + String responseString = null; + try { + responseString = EntityUtils.toString(entity, "UTF-8"); + } catch (IOException e) { + throw new RuntimeException(e); + } + JSONObject result = new JSONObject(responseString); + return result; + } + + @AfterMethod + private void closeAuditLog() { + auditLog.clearAuditLog(rmAdmin.get().getUsername(),rmAdmin.get().getPassword()); + } + + @AfterClass(alwaysRun = true) + private void electronicRecordAuditLogCleanup() { + deleteRecord(electronicRecord.getId()); + deleteRecordFolder(recordFolder1.getId()); + deleteRecordCategory(category1.getId()); + dataUser.usingAdmin().deleteUser(new UserModel(rmAdmin.get().getUsername(), rmAdmin.get().getPassword())); + } +} \ No newline at end of file diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/audit/NonElectronicRecordAuditLogTest.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/audit/NonElectronicRecordAuditLogTest.java new file mode 100644 index 0000000000..8b5d6d0c2d --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/audit/NonElectronicRecordAuditLogTest.java @@ -0,0 +1,246 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rm.community.audit; + +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.audit.AuditEntry; +import org.alfresco.rest.rm.community.model.record.Record; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.v0.RMAuditAPI; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.rest.v0.RecordsAPI; +import org.alfresco.test.AlfrescoTest; +import org.alfresco.utility.Utility; +import org.alfresco.utility.model.UserModel; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.util.EntityUtils; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.AssertJUnit; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAspects.ASPECTS_COMPLETED_RECORD; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.createRecordModel; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.test.util.AssertionErrors.assertTrue; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.fail; + + +public class NonElectronicRecordAuditLogTest extends BaseRMRestTest { + private Optional rmAdmin; + @Autowired + private RMRolesAndActionsAPI rmRolesAndActionsAPI; + @Autowired + private RMAuditAPI auditLog; + @Autowired + private RecordsAPI recordApi; + private RecordCategory category1; + private RecordCategoryChild recordFolder1; + private Record nonElectronicRecord , nonElectronicRecord2; + private static final String AUDIT_NON_ELECTRONIC_RECORD = generateTestPrefix(NonElectronicRecordAuditLogTest.class) + "non electronic record"; + private static final String AUDIT_COMPLETE_REOPEN_NON_ELECTRONIC_RECORD = "Complete Reopen Non-Electronic Record"; + public static final String TITLE = "Title"; + public static final String DESCRIPTION = "Description"; + + @BeforeClass(alwaysRun = true) + public void nonElectronicRecordAuditLogSetup() + { + createRMSiteIfNotExists(); + rmAdmin = Optional.ofNullable(getDataUser().createRandomTestUser()); + rmRolesAndActionsAPI.assignRoleToUser( + getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), + rmAdmin.get().getUsername(), + "Administrator"); + + auditLog.clearAuditLog(rmAdmin.get().getUsername(),rmAdmin.get().getPassword()); + category1 = createRootCategory(TITLE, DESCRIPTION); + recordFolder1 = createFolder(category1.getId(),TITLE); + nonElectronicRecord = createNonElectronicRecord(recordFolder1.getId(),AUDIT_NON_ELECTRONIC_RECORD,rmAdmin.get()); + + } + + @Test(description = "Audit log for newly filed non-electronic record") + @AlfrescoTest(jira="RM-4303") + public void newNonElectronicRecordAudit() + { + + List auditEntries= auditLog.getRMAuditLogAll(getAdminUser().getUsername(),getAdminUser().getPassword(),100); + + // newly created record contains 3 events: "created object", "file to" and metadata update + assertTrue("File To Event is not present.",auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("File to"))); + assertTrue("Updated metadata Event is not present.",auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Updated Metadata"))); + assertTrue("Created Object Event is not present.",auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Created Object"))); + } + + @Test + ( + dependsOnMethods = "newNonElectronicRecordAudit", + description = "Viewing Non electronic record audit log is itself an auditable event" + ) + @AlfrescoTest(jira="RM-4303") + public void nonElectronicRecordAuditIsEvent() + { + List auditEntries= auditLog.getRMAuditLogAll(getAdminUser().getUsername(),getAdminUser().getPassword(),100); + assertTrue("Audit View Event is not present.",auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Audit View"))); + + } + + @Test + ( + dependsOnMethods = "nonElectronicRecordAuditIsEvent", + description = "Rename electronic record is an edit metadata event" + ) + @AlfrescoTest(jira="RM-4303") + public void renameNonElectronicRecord() + { + auditLog.clearAuditLog(rmAdmin.get().getUsername(),rmAdmin.get().getPassword()); + Record renameNonElectronicRecord = createRecordModel("edited " + nonElectronicRecord.getName(), "", ""); + + // rename record + getRestAPIFactory().getRecordsAPI().updateRecord(renameNonElectronicRecord, nonElectronicRecord.getId()); + assertStatusCode(OK); + + // we expect 1 new event: "metadata update" + List auditEntries= auditLog.getRMAuditLogAll(getAdminUser().getUsername(),getAdminUser().getPassword(),100); + assertTrue("Updated metadata Event is not present.",auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Updated Metadata"))); + + } + + @Test (dependsOnMethods = "newNonElectronicRecordAudit",description = "Complete and reopen electronic record") + @AlfrescoTest(jira="RM-4303") + public void completeAndReopenNonElectronicRecord() + { + nonElectronicRecord2 = createNonElectronicRecord(recordFolder1.getId(),AUDIT_COMPLETE_REOPEN_NON_ELECTRONIC_RECORD); + + // complete record + recordApi.completeRecord(rmAdmin.get().getUsername(),rmAdmin.get().getPassword(), + nonElectronicRecord2.getName()); + + try + { + Utility.sleep(1000, 30000, () -> + { + org.alfresco.rest.rm.community.requests.gscore.api.RecordsAPI recordsAPI = getRestAPIFactory().getRecordsAPI(); + List aspects = recordsAPI.getRecord(nonElectronicRecord2.getId()).getAspectNames(); + // a record must be completed + assertTrue("Record is not completed.",aspects.contains(ASPECTS_COMPLETED_RECORD)); + }); + } + catch (InterruptedException e) + { + fail("InterruptedException received while waiting for results."); + } + + List auditEntries= auditLog.getRMAuditLogAll(getAdminUser().getUsername(),getAdminUser().getPassword(),100); + assertTrue("Complete Record Event is not present.",auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Complete Record"))); + + // Reopen record + recordApi.reOpenRecord(rmAdmin.get().getUsername(),rmAdmin.get().getPassword(), + nonElectronicRecord2.getName()); + + try + { + Utility.sleep(1000, 30000, () -> + { + org.alfresco.rest.rm.community.requests.gscore.api.RecordsAPI recordsAPI = getRestAPIFactory().getRecordsAPI(); + List aspects = recordsAPI.getRecord(nonElectronicRecord2.getId()).getAspectNames(); + // a record mustn't be completed + assertFalse(aspects.contains(ASPECTS_COMPLETED_RECORD)); + }); + } + catch (InterruptedException e) + { + fail("InterruptedException received while waiting for results."); + } + + auditEntries= auditLog.getRMAuditLogAll(getAdminUser().getUsername(),getAdminUser().getPassword(),100); + assertTrue("Reopen Record Event is not present.",auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Reopen Record"))); + + } + @Test + ( + dependsOnMethods = "completeAndReopenNonElectronicRecord", + description = "File electronic record's audit log as record" + ) + @AlfrescoTest(jira="RM-4303") + public void fileNonElectronicRecordAuditLogAsRecord() + { + // audit log is stored in the same folder, refresh it so that it appears in the list + HttpResponse auditRecordHttpResponse = auditLog.logsAuditLogAsRecord(rmAdmin.get().getUsername(),rmAdmin.get().getPassword(), + getRecordNodeRef(nonElectronicRecord2.getId()),getFolderNodeRef(recordFolder1.getId())); + JSONObject auditRecordProperties = getAuditPropertyValues(auditRecordHttpResponse); + Record auditRecord = getRestAPIFactory().getRecordsAPI().getRecord(auditRecordProperties.get("record").toString() + .replace("workspace://SpacesStore/","")); + // check audit log + AssertJUnit.assertTrue(auditRecordProperties.get("recordName").toString().endsWith(".html")); + AssertJUnit.assertTrue(auditRecord.getAspectNames().stream().noneMatch(x -> x.startsWith(ASPECTS_COMPLETED_RECORD))); + + } + + private String getFolderNodeRef(String folderId) { + return "workspace://SpacesStore/" + folderId; + } + + private String getRecordNodeRef(String recordId) { + return "workspace/SpacesStore/" + recordId; + } + + private JSONObject getAuditPropertyValues(HttpResponse httpResponse) { + HttpEntity entity = httpResponse.getEntity(); + String responseString = null; + try { + responseString = EntityUtils.toString(entity, "UTF-8"); + } catch (IOException e) { + throw new RuntimeException(e); + } + JSONObject result = new JSONObject(responseString); + return result; + } + @AfterMethod + private void closeAuditLog() { + auditLog.clearAuditLog(rmAdmin.get().getUsername(),rmAdmin.get().getPassword()); + } + + @AfterClass(alwaysRun = true) + private void nonElectronicRecordAuditLogCleanup() { + deleteRecord(nonElectronicRecord.getId()); + deleteRecord(nonElectronicRecord2.getId()); + deleteRecordFolder(recordFolder1.getId()); + deleteRecordCategory(category1.getId()); + dataUser.usingAdmin().deleteUser(new UserModel(rmAdmin.get().getUsername(), rmAdmin.get().getPassword())); + } +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/audit/RecordCategoryAuditLogTest.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/audit/RecordCategoryAuditLogTest.java new file mode 100644 index 0000000000..f2ec8562d0 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/audit/RecordCategoryAuditLogTest.java @@ -0,0 +1,120 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rm.community.audit; + +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.audit.AuditEntry; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.v0.RMAuditAPI; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.test.AlfrescoTest; +import org.alfresco.utility.model.UserModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.List; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.utility.data.RandomData.getRandomAlphanumeric; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.test.util.AssertionErrors.assertTrue; + +public class RecordCategoryAuditLogTest extends BaseRMRestTest { + @Autowired + private RMRolesAndActionsAPI rmRolesAndActionsAPI; + @Autowired + private RMAuditAPI auditLog; + + private final String TEST_PREFIX = generateTestPrefix(RecordCategoryAuditLogTest.class); + private final String RM_ADMIN = TEST_PREFIX + "rm_admin"; + private static final String AUDIT_CATEGORY = generateTestPrefix(RecordCategoryAuditLogTest.class) + "category"; + private RecordCategory recordCategoryAudit; + + @BeforeClass(alwaysRun = true) + public void recordCategoryAuditLogSetup() { + STEP("Create RM Site"); + createRMSiteIfNotExists(); + + STEP("Create RM Admin user"); + rmRolesAndActionsAPI.createUserAndAssignToRole(getAdminUser().getUsername(), getAdminUser().getPassword(), RM_ADMIN, + getAdminUser().getPassword(), + "Administrator"); + } + + @Test + @AlfrescoTest(jira = "RM-2768") + public void recordCategoryAudit() throws Exception { + STEP("Create root level category"); + recordCategoryAudit = createRootCategory(AUDIT_CATEGORY); + List auditEntries = auditLog.getRMAuditLogAll(getAdminUser().getUsername(), getAdminUser().getPassword(), 100); + // newly created record category contains 3 events: object creation, inherited permissions set to false and metadata update + // the order in which object creation and metadata update are listed isn't always identical due to + // both happening in the same transaction + assertTrue("Created Object Event is not present.", auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Created Object"))); + assertTrue("Updated metadata Event is not present.", auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Updated Metadata"))); + } + + @Test + ( + dependsOnMethods = "recordCategoryAudit", + description = "Viewing audit log is itself an auditable event" + ) + @AlfrescoTest(jira="RM-4303") + public void recordCategoryAuditIsEvent() { + List auditEntries = auditLog.getRMAuditLogAll(getAdminUser().getUsername(), getAdminUser().getPassword(), 100); + assertTrue("Audit View Event is not present.", auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Audit View"))); + } + + @Test + ( + dependsOnMethods = "recordCategoryAuditIsEvent", + description = "Record category rename is an edit metadata event" + ) + @AlfrescoTest(jira="RM-4303") + public void renameRecordCategory() { + String categoryName = "Category name " + getRandomAlphanumeric(); + RecordCategory rootRecordCategory = createRootCategory(categoryName); + String newCategoryName = "Rename " + categoryName; + RecordCategory recordCategoryUpdated = RecordCategory.builder().name(newCategoryName).build(); + RecordCategory renamedRecordCategory = getRestAPIFactory().getRecordCategoryAPI().updateRecordCategory(recordCategoryUpdated, rootRecordCategory.getId()); + + assertStatusCode(OK); + // we expect 1 new event: "metadata update" + List auditEntries = auditLog.getRMAuditLogAll(getAdminUser().getUsername(), getAdminUser().getPassword(), 100); + assertTrue("Updated metadata Event is not present.", auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Updated Metadata"))); + } + @AfterClass(alwaysRun = true) + private void electronicRecordAuditLogCleanup() { + deleteRecordCategory(recordCategoryAudit.getId()); + dataUser.deleteUser(new UserModel(RM_ADMIN, + getAdminUser().getPassword())); + auditLog.clearAuditLog(getAdminUser().getUsername(), getAdminUser().getPassword()); + } +} + diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/audit/RecordFolderAuditLogTest.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/audit/RecordFolderAuditLogTest.java new file mode 100644 index 0000000000..f8bf2e2686 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/audit/RecordFolderAuditLogTest.java @@ -0,0 +1,175 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rm.community.audit; + +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.audit.AuditEntry; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.rm.community.model.recordfolder.RecordFolder; +import org.alfresco.rest.v0.RMAuditAPI; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.rest.v0.RecordFoldersAPI; +import org.alfresco.test.AlfrescoTest; +import org.alfresco.utility.Utility; +import org.alfresco.utility.model.UserModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import java.util.List; +import java.util.Optional; + +import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAspects.ASPECTS_COMPLETED_RECORD; +import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.createRecordFolderModel; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.test.util.AssertionErrors.assertTrue; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.fail; + +public class RecordFolderAuditLogTest extends BaseRMRestTest { + + private Optional rmAdmin; + @Autowired + private RMRolesAndActionsAPI rmRolesAndActionsAPI; + @Autowired + private RMAuditAPI auditLog; + + @Autowired + private RecordFoldersAPI recordFoldersAPI; + private RecordCategory category1; + private RecordCategoryChild recordFolder1; + public static final String TITLE = "Title"; + public static final String DESCRIPTION = "Description"; + + @BeforeClass(alwaysRun = true) + public void recordFolderAuditLogSetup() { + createRMSiteIfNotExists(); + rmAdmin = Optional.ofNullable(getDataUser().createRandomTestUser()); + rmRolesAndActionsAPI.assignRoleToUser( + getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), + rmAdmin.get().getUsername(), + "Administrator"); + } + + @Test(description = "Audit log for empty record folder") + @AlfrescoTest(jira = "RM-4303") + public void recordFolderAudit() { + category1 = createRootCategory(TITLE, DESCRIPTION); + recordFolder1 = createFolder(category1.getId(), TITLE); + List auditEntries = auditLog.getRMAuditLogAll(getAdminUser().getUsername(), getAdminUser().getPassword(), 100); + assertTrue("Created Object Event is not present.", auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Created Object"))); + assertTrue("Updated metadata Event is not present.", auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Updated Metadata"))); + + } + + @Test + ( + dependsOnMethods = "recordFolderAudit", + description = "Viewing record folder audit log is itself an auditable event" + ) + @AlfrescoTest(jira = "RM-4303") + public void recordFolderAuditIsEvent() { + List auditEntries = auditLog.getRMAuditLogAll(getAdminUser().getUsername(), getAdminUser().getPassword(), 100); + assertTrue("Audit View Event is not present.", auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Audit View"))); + + } + + @Test + ( + dependsOnMethods = "recordFolderAuditIsEvent", + description = "Record folder rename is an edit metadata event" + ) + @AlfrescoTest(jira = "RM-4303") + public void renameRecordFolder() { + auditLog.clearAuditLog(rmAdmin.get().getUsername(), rmAdmin.get().getPassword()); + RecordFolder renameRecordFolder = createRecordFolderModel(category1.getId(), "edited"); + getRestAPIFactory().getRecordFolderAPI().updateRecordFolder(renameRecordFolder, recordFolder1.getId()); + assertStatusCode(OK); + // we expect 1 new event: "metadata update" + List auditEntries = auditLog.getRMAuditLogAll(getAdminUser().getUsername(), getAdminUser().getPassword(), 100); +// assertTrue("Move To Event is not present.",auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Move to"))); + assertTrue("Updated metadata Event is not present.", auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Updated Metadata"))); + + } + + @Test(dependsOnMethods = "recordFolderAudit", + description = "Close and reopen folder") + @AlfrescoTest(jira = "RM-4303") + public void closeReopenFolder() { + //close folder + recordFoldersAPI.closeRecordFolder(rmAdmin.get().getUsername(), rmAdmin.get().getPassword(), + recordFolder1.getName()); + try + { + Utility.sleep(1000, 30000, () -> + { + List auditEntries = auditLog.getRMAuditLogAll(getAdminUser().getUsername(), getAdminUser().getPassword(), 100); + assertTrue("Folder Close Record Event is not present.", auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Close Record Folder"))); + + }); + } + catch (InterruptedException e) + { + fail("InterruptedException received while waiting for results."); + } + + //reopen folder + recordFoldersAPI.reOpenRecordFolder(rmAdmin.get().getUsername(), rmAdmin.get().getPassword(), + recordFolder1.getName()); + try + { + Utility.sleep(1000, 30000, () -> + { + + List auditEntries = auditLog.getRMAuditLogAll(getAdminUser().getUsername(), getAdminUser().getPassword(), 100); + assertTrue("Reopen Record Event is not present.", auditEntries.stream().anyMatch(x -> x.getEvent().startsWith("Open Record Folder"))); + + }); + } + catch (InterruptedException e) + { + fail("InterruptedException received while waiting for results."); + } + } + @AfterMethod + private void closeAuditLog() + { + auditLog.clearAuditLog(rmAdmin.get().getUsername(),rmAdmin.get().getPassword()); + } + + @AfterClass (alwaysRun = true) + public void recordFolderAuditLogCleanup() + { + deleteRecordFolder(recordFolder1.getId()); + deleteRecordCategory(category1.getId()); + dataUser.usingAdmin().deleteUser(new UserModel(rmAdmin.get().getUsername(), rmAdmin.get().getPassword())); + } + +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/base/BaseRMRestTest.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/base/BaseRMRestTest.java index 8830ad3fc7..560a39b4c8 100644 --- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/base/BaseRMRestTest.java +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/base/BaseRMRestTest.java @@ -53,9 +53,12 @@ import static org.springframework.http.HttpStatus.OK; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Date; +import java.util.TimeZone; import java.util.stream.Collectors; import lombok.Getter; @@ -91,6 +94,7 @@ import org.alfresco.utility.model.FileModel; import org.alfresco.utility.model.FolderModel; import org.alfresco.utility.model.SiteModel; import org.alfresco.utility.model.UserModel; +import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.testng.annotations.BeforeClass; @@ -121,6 +125,8 @@ public class BaseRMRestTest extends RestTest @Getter(value = PROTECTED) private SearchAPI searchApi; + protected static final String iso8601_DateFormat="yyyy-MM-dd'T'HH:mm:ss.SSSXXX"; + /** * Asserts the given status code * @@ -161,7 +167,7 @@ public class BaseRMRestTest extends RestTest */ @Override @BeforeClass (alwaysRun = true) - public void checkServerHealth() throws Exception + public void checkServerHealth() { // Create RM Site if not exist createRMSiteIfNotExists(); @@ -628,8 +634,8 @@ public class BaseRMRestTest extends RestTest * Returns search results for the given search term * * @param user - * @param term - * @param query language + * @param q + * @param queryLanguage language * @return * @throws Exception */ @@ -956,5 +962,34 @@ public class BaseRMRestTest extends RestTest return false; } } + /** + * Helper method to get the Previous Date in the YYYY-MM-ddTHH:mm:ss.SSSXXX format + * @param previousDays number of previous days while calculating the date as output + * @return previousDate as String in the ISO 8601 Date Format + */ + protected String getIso8601Date(int previousDays) { + Date date = new Date(System.currentTimeMillis()); + Date previousDate = new Date(date.getTime() - previousDays); + // Conversion + SimpleDateFormat sdf= new SimpleDateFormat(iso8601_DateFormat);; + sdf.setTimeZone(TimeZone.getDefault()); + return sdf.format(previousDate); + } + /** + * Helper method to provide the Edited Disposition Date Json + * The Edited Disposition Date is modified to previous date so that CUTOFF & DESTROY Steps will be enabled + * @return JsonObject with the format {"name":"editDispositionActionAsOfDate","params":{"asOfDate":{"iso8601":"Previous Date"}}} + */ + protected JSONObject editDispositionDateJson() { + JSONObject requestParams = new JSONObject(); + requestParams.put("name","editDispositionActionAsOfDate"); + JSONObject params = new JSONObject(); + requestParams.put("params",params); + + JSONObject asOfDate = new JSONObject(); + params.put("asOfDate",asOfDate); + asOfDate.put("iso8601",getIso8601Date(1)); + return requestParams; + } } diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/files/DeclareAndFileDocumentAsRecordTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/files/DeclareAndFileDocumentAsRecordTests.java index 22e2c5ed8d..239150aeab 100644 --- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/files/DeclareAndFileDocumentAsRecordTests.java +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/files/DeclareAndFileDocumentAsRecordTests.java @@ -72,6 +72,7 @@ import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; +import org.testng.annotations.Ignore; import org.testng.annotations.Test; import org.alfresco.utility.model.TestGroup; @@ -259,6 +260,7 @@ public class DeclareAndFileDocumentAsRecordTests extends BaseRMRestTest * And the document is not declared as a record */ @Test (dataProvider = "invalidDestinationPaths",groups = { TestGroup.NOT_SUPPORTED_ON_SINGLE_PIPELINE }) + @Ignore public void declareAndFileToInvalidLocationUsingActionsAPI(String containerPath, String expectedException) throws Exception { STEP("Declare document as record with an invalid location parameter value"); diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/files/FileVersionAsRecordTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/files/FileVersionAsRecordTests.java index b17d6de141..f3c89606d4 100644 --- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/files/FileVersionAsRecordTests.java +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/files/FileVersionAsRecordTests.java @@ -61,6 +61,7 @@ import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; +import org.testng.annotations.Ignore; import org.testng.annotations.Test; import org.alfresco.utility.model.TestGroup; @@ -209,6 +210,7 @@ public class FileVersionAsRecordTests extends BaseRMRestTest * And the document is not declared as a version record */ @Test (dataProvider = "invalidDestinationPaths", groups = { TestGroup.NOT_SUPPORTED_ON_SINGLE_PIPELINE }) + @Ignore public void declareVersionAndFileToInvalidLocationUsingActionsAPI(String containerPath, String expectedException) throws Exception { STEP("Declare document as record version with an invalid location parameter value"); diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/records/DeclareInPlaceRecordsTestLevel2.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/records/DeclareInPlaceRecordsTestLevel2.java new file mode 100644 index 0000000000..c871abd17c --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/records/DeclareInPlaceRecordsTestLevel2.java @@ -0,0 +1,311 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.rm.community.records; + +import org.alfresco.dataprep.CMISUtil; +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.record.Record; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.rm.community.model.rules.ActionsOnRule; +import org.alfresco.rest.rm.community.model.rules.RuleDefinition; +import org.alfresco.rest.rm.community.model.unfiledcontainer.UnfiledContainer; +import org.alfresco.rest.rm.community.model.unfiledcontainer.UnfiledContainerChildCollection; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.rest.v0.RecordsAPI; +import org.alfresco.rest.v0.RulesAPI; +import org.alfresco.rest.v0.service.DispositionScheduleService; +import org.alfresco.test.AlfrescoTest; +import org.alfresco.utility.constants.UserRole; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.UserModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import static java.util.Arrays.asList; +import static org.alfresco.rest.core.v0.BaseAPI.NODE_PREFIX; +import static org.alfresco.rest.core.v0.BaseAPI.RM_SITE_ID; +import static org.alfresco.rest.rm.community.base.TestData.DEFAULT_PASSWORD; +import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAlias.UNFILED_RECORDS_CONTAINER_ALIAS; +import static org.alfresco.rest.rm.community.model.recordcategory.RetentionPeriodProperty.CREATED_DATE; +import static org.alfresco.rest.rm.community.model.user.UserPermissions.PERMISSION_READ_RECORDS; +import static org.alfresco.rest.rm.community.requests.gscore.api.FilesAPI.PARENT_ID_PARAM; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.junit.Assert.assertTrue; +import static org.springframework.http.HttpStatus.CREATED; + +public class DeclareInPlaceRecordsTestLevel2 extends BaseRMRestTest { + private final String TEST_PREFIX = generateTestPrefix(DeclareInPlaceRecordsTestLevel2.class); + private final String RM_ADMIN = TEST_PREFIX + "rm_admin"; + private final String RECORDS_CATEGORY_ONE = TEST_PREFIX + "category"; + public static final String RECORD_FOLDER_ONE = "record-folder-one"; + public static final String RECORD_CATEGORY_TWO = "record-category-two" + System.currentTimeMillis(); + public static final String RECORD_FOLDER_TWO = "record-folder-two"; + private final String RULE_NAME = TEST_PREFIX + "rule unfiled"; + private String unfiledRecordsNodeRef; + private RecordCategory RecordCategoryOne, RecordCategoryTwo; + private RecordCategoryChild recordFolder; + private UnfiledContainer unfiledContainer; + private FolderModel testFolder; + private SiteModel testSite; + private SiteModel privateSite; + private UserModel testUser; + @Autowired + private DispositionScheduleService dispositionScheduleService; + @Autowired + private RulesAPI rulesAPI; + /** + * data prep services + */ + @Autowired + private RMRolesAndActionsAPI rmRolesAndActionsAPI; + @Autowired + private RecordsAPI recordsAPI; + + @BeforeClass(alwaysRun = true) + public void preConditions() { + STEP("Create RM Site"); + createRMSiteIfNotExists(); + privateSite = dataSite.usingAdmin().createPrivateRandomSite(); + } + + /** + * Given that a user is the owner of a document + * And that user has been deleted + * When admin tries to declare the document as a record + * Then the document becomes an inplace record + */ + @Test + @AlfrescoTest(jira="RM-2584") + public void DeclareRecordOwnerDeleted() throws Exception { + + createTestPrecondition(); + + // Upload document in a folder in a collaboration site + FileModel uploadedDoc = dataContent.usingSite(testSite) + .usingUser(testUser) + .usingResource(testFolder) + .createContent(CMISUtil.DocumentType.TEXT_PLAIN); + + // delete the test user + dataUser.deleteUser(testUser); + + // declare uploadedDocument as record + getRestAPIFactory().getFilesAPI(getDataUser().getAdminUser()).declareAsRecord(uploadedDoc.getNodeRefWithoutVersion()); + assertStatusCode(CREATED); + + // assert that the document is now a record + assertTrue(hasRecordAspect(uploadedDoc)); + } + /** + * Given that a user is the owner of a document + * And that user declare the document as a record + * When admin files the record to a category that has a disposition schedule applied on records and a cut off step + * And admin completes the record so the pending record action is now Cut off + * Then user is still able to see the in place record in original share site location + */ + @Test + @AlfrescoTest(jira="MNT-18558") + public void inPlaceRecordVisibilityAfterFilingToCategoryWithCutOffStep() throws Exception { + + // create test precondition + createTestPrecondition(RECORDS_CATEGORY_ONE); + + //create a disposition schedule on Records level with a cut off step + dispositionScheduleService.createCategoryRetentionSchedule(RECORDS_CATEGORY_ONE, true); + dispositionScheduleService.addCutOffAfterPeriodStep(RECORDS_CATEGORY_ONE, "day|2", CREATED_DATE); + + //create a folder in category + recordFolder = createFolder(getAdminUser(),RecordCategoryOne.getId(),RECORD_FOLDER_ONE); + + // create a File to record folder rule applied on Unfiled Records container + fileToRuleAppliedOnUnfiledRecords(); + + //create a new test user + UserModel testUser = createSiteManager(); + + // upload a new document as the user and declare the document as record + FileModel uploadedDoc = dataContent.usingSite(privateSite) + .usingUser(testUser) + .createContent(CMISUtil.DocumentType.TEXT_PLAIN); + + Record uploadedRecord = getRestAPIFactory().getFilesAPI(getDataUser().getAdminUser()).declareAsRecord(uploadedDoc.getNodeRefWithoutVersion()); + assertStatusCode(CREATED); + + //Complete the record as admin to be sure that pending action is now Cut off + recordsAPI.completeRecord(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), uploadedRecord.getName()); + + // As test user navigate to collaboration site documents library and check that the record is still visible + dataContent.usingAdmin().usingSite(privateSite).assertContentExist(); + } + /** + * Create a user called test + * Create a collaboration site + * Add the user to the collaboration site as consumer + * Create an RM site + * In RM, create a new categories under file plan with a cut off step set after an event happens + * Under the previously created category create a folder + * Set READ-ONLY permission for test user for the folder previously created (the user does not have Read + * permissions to the category containing the folder) + * In the collaboration site create two files + * File as record the first file + * Log in with test user and check if he can still see the two files in the collaboration site + */ + @Test + @AlfrescoTest (jira = "MNT-22138") + public void filesVisibilityAfterFilingToCategoryWithCutOffAfterEventStep() throws Exception { + + //create a category + RecordCategoryTwo = createRootCategory(RECORD_CATEGORY_TWO); + + //create a disposition schedule on Records level with a cut off step + dispositionScheduleService.createCategoryRetentionSchedule(RECORD_CATEGORY_TWO, true); + dispositionScheduleService.addCutOffAfterPeriodStep(RECORD_CATEGORY_TWO, "day|2", CREATED_DATE); + + //create a folder in category + recordFolder = createFolder(getAdminUser(),RecordCategoryTwo.getId(),RECORD_FOLDER_TWO); + + //create a new test user + UserModel siteConsumer = getDataUser().createRandomTestUser(); + getDataUser().addUserToSite(siteConsumer,privateSite,UserRole.SiteConsumer); + + // give read permissions to test user + getRestAPIFactory().getRMUserAPI().addUserPermission(recordFolder.getId(), siteConsumer, PERMISSION_READ_RECORDS); + + // create two documents + FileModel testFile = dataContent.usingSite(new SiteModel(privateSite.getTitle())) + .usingAdmin() + .createContent(CMISUtil.DocumentType.TEXT_PLAIN); + + FileModel testFileNotFiled = dataContent.usingSite(new SiteModel(privateSite.getTitle())) + .usingAdmin() + .createContent(CMISUtil.DocumentType.TEXT_PLAIN); + + // file one of the documents as record + getRestAPIFactory().getFilesAPI() + .usingParams(String.format("%s=%s", PARENT_ID_PARAM, recordFolder.getId())) + .declareAsRecord(testFile.getNodeRefWithoutVersion()); + getRestAPIFactory().getRmRestWrapper().assertStatusCodeIs(CREATED); + + // As test user navigate to collaboration site documents library and check that both of the documents are + // visible + STEP("Verify the document in collaboration site is now a record"); + Assert.assertTrue(hasRecordAspect(testFile), "File should have record aspect"); + Assert.assertFalse(hasRecordAspect(testFileNotFiled), "File should not have record aspect"); + } + + private void createTestPrecondition(String categoryName) { + + // create "rm admin" user if it does not exist and assign it to RM Administrator role + rmRolesAndActionsAPI.createUserAndAssignToRole( + getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), + RM_ADMIN, DEFAULT_PASSWORD, "Administrator"); + + // create category + STEP("Create category"); + RecordCategoryOne = createRootCategory(categoryName,"Title"); + + unfiledContainer = getRestAPIFactory().getUnfiledContainersAPI().getUnfiledContainer(UNFILED_RECORDS_CONTAINER_ALIAS); + + unfiledRecordsNodeRef = NODE_PREFIX + unfiledContainer.getId(); + } + private void createTestPrecondition() { + STEP("Create collab_user user"); + testUser = getDataUser().createRandomTestUser(); + testSite = dataSite.usingAdmin().createPublicRandomSite(); + + // invite collab_user to Collaboration site with Contributor role + getDataUser().addUserToSite(testUser, testSite, UserRole.SiteContributor); + + testFolder = dataContent.usingSite(testSite).usingUser(testUser).createFolder(); + } + private void fileToRuleAppliedOnUnfiledRecords() { + unfiledRecordsRuleTeardown(); + + // create a rule + RuleDefinition ruleDefinition = RuleDefinition.createNewRule().title(RULE_NAME) + .description(RULE_NAME) + .createRecordPath(false) + .path("/" + RECORDS_CATEGORY_ONE + "/" + RECORD_FOLDER_ONE) + .runInBackground(true) + .actions(asList(ActionsOnRule.FILE_TO.getActionValue())); + + // create a rule on unfiledRecords + rulesAPI.createRule(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), unfiledRecordsNodeRef, ruleDefinition); + } + private void unfiledRecordsRuleTeardown() { + rulesAPI.deleteAllRulesOnContainer(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), unfiledRecordsNodeRef); + } + public UserModel createSiteManager() { + UserModel siteManager = getDataUser().createRandomTestUser(); + getDataUser().addUserToSite(siteManager, privateSite, UserRole.SiteManager); + return siteManager; + } + @AfterClass + public void cleanupCategory() { + unfiledRecordsRuleTeardown(); + rmRolesAndActionsAPI.deleteAllItemsInContainer(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), RM_SITE_ID, RECORD_FOLDER_ONE); + rmRolesAndActionsAPI.deleteAllItemsInContainer(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), RM_SITE_ID, RecordCategoryOne.getName()); + deleteRecordCategory(RecordCategoryOne.getId()); + + rmRolesAndActionsAPI.deleteAllItemsInContainer(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), RM_SITE_ID, RECORD_FOLDER_TWO); + rmRolesAndActionsAPI.deleteAllItemsInContainer(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), RM_SITE_ID, RecordCategoryTwo.getName()); + deleteRecordCategory(RecordCategoryTwo.getId()); + + dataSite.usingAdmin().deleteSite(privateSite); + dataSite.usingAdmin().deleteSite(testSite); + + UnfiledContainerChildCollection unfiledContainerChildCollection = getRestAPIFactory() + .getUnfiledContainersAPI().getUnfiledContainerChildren(unfiledContainer.getId()); + + unfiledContainerChildCollection.getEntries().forEach(unfiledChild -> + { + if (unfiledChild.getEntry().getIsRecord()) + { + getRestAPIFactory().getRecordsAPI().deleteRecord(unfiledChild.getEntry().getId()); + } + else + { + getRestAPIFactory().getUnfiledRecordFoldersAPI().deleteUnfiledRecordFolder(unfiledChild.getEntry().getId()); + } + }); + } +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/records/DeleteRecordTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/records/DeleteRecordTests.java index 8a6a500e30..79fba332fb 100644 --- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/records/DeleteRecordTests.java +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/records/DeleteRecordTests.java @@ -75,6 +75,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; +import org.testng.annotations.Ignore; import org.testng.annotations.Test; /** @@ -102,7 +103,7 @@ public class DeleteRecordTests extends BaseRMRestTest testSite = dataSite.usingAdmin().createPublicRandomSite(); recordFolder = createCategoryFolderInFilePlan(); unfiledRecordFolder = createUnfiledContainerChild(UNFILED_RECORDS_CONTAINER_ALIAS, getRandomName("Unfiled Folder "), - UNFILED_RECORD_FOLDER_TYPE); + UNFILED_RECORD_FOLDER_TYPE); } /** Data provider with electronic and non-electronic records to be deleted */ @@ -133,10 +134,10 @@ public class DeleteRecordTests extends BaseRMRestTest * */ @Test - ( - dataProvider = "recordsToBeDeleted", - description = "Admin user can delete records" - ) + ( + dataProvider = "recordsToBeDeleted", + description = "Admin user can delete records" + ) @AlfrescoTest(jira="RM-4363") public void adminCanDeleteRecords(String recordId) { @@ -154,17 +155,17 @@ public class DeleteRecordTests extends BaseRMRestTest * */ @Test - ( - description = "User without write permissions can't delete a record" - ) + ( + description = "User without write permissions can't delete a record" + ) @AlfrescoTest(jira="RM-4363") public void userWithoutWritePermissionsCantDeleteRecord() { // Create a non-electronic record in unfiled records UnfiledContainerChild nonElectronicRecord = UnfiledContainerChild.builder() - .name("Record " + RandomData.getRandomAlphanumeric()) - .nodeType(NON_ELECTRONIC_RECORD_TYPE) - .build(); + .name("Record " + RandomData.getRandomAlphanumeric()) + .nodeType(NON_ELECTRONIC_RECORD_TYPE) + .build(); UnfiledContainerChild newRecord = getRestAPIFactory().getUnfiledContainersAPI().createUnfiledContainerChild(nonElectronicRecord, UNFILED_RECORDS_CONTAINER_ALIAS); assertStatusCode(CREATED); @@ -187,9 +188,9 @@ public class DeleteRecordTests extends BaseRMRestTest * */ @Test - ( - description = "User without delete records capability can't delete a record" - ) + ( + description = "User without delete records capability can't delete a record" + ) @AlfrescoTest(jira="RM-4363") public void userWithoutDeleteRecordsCapabilityCantDeleteRecord() { @@ -234,7 +235,7 @@ public class DeleteRecordTests extends BaseRMRestTest STEP("Create a record in first folder and copy it into second folder."); String recordId = getRestAPIFactory().getRecordFolderAPI() - .createRecord(createElectronicRecordModel(), recordFolder.getId(), getFile(IMAGE_FILE)).getId(); + .createRecord(createElectronicRecordModel(), recordFolder.getId(), getFile(IMAGE_FILE)).getId(); String copyId = copyNode(recordId, recordFolderB.getId()).getId(); assertStatusCode(CREATED); @@ -318,14 +319,14 @@ public class DeleteRecordTests extends BaseRMRestTest RecordCategoryChild recFolder = createFolder(recordCategory.getId(), getRandomName("recFolder")); RecordBodyFile recordBodyFile = RecordBodyFile.builder().targetParentId(recFolder.getId()).build(); Record recordFiled = getRestAPIFactory().getRecordsAPI().fileRecord(recordBodyFile, testFile.getNodeRefWithoutVersion()); - getRestAPIFactory().getRecordsAPI().completeRecord(recordFiled.getId()); - assertStatusCode(CREATED); + completeRecord(recordFiled.getId()); + assertStatusCode(OK); STEP("Execute the disposition schedule steps."); rmRolesAndActionsAPI.executeAction(getAdminUser().getUsername(), getAdminUser().getUsername(), recordFiled.getName(), - RM_ACTIONS.CUT_OFF); + RM_ACTIONS.CUT_OFF); rmRolesAndActionsAPI.executeAction(getAdminUser().getUsername(), getAdminUser().getUsername(), recordFiled.getName(), - RM_ACTIONS.DESTROY); + RM_ACTIONS.DESTROY); STEP("Check that it's possible to load the copy content."); getNodeContent(copy.getId()); @@ -348,14 +349,14 @@ public class DeleteRecordTests extends BaseRMRestTest STEP("Declare file version as record."); recordsAPI.declareDocumentVersionAsRecord(getAdminUser().getUsername(), getAdminUser().getPassword(), testSite.getId(), - testFile.getName()); + testFile.getName()); UnfiledContainerChild unfiledContainerChild = getRestAPIFactory().getUnfiledContainersAPI() - .getUnfiledContainerChildren(UNFILED_RECORDS_CONTAINER_ALIAS) - .getEntries().stream() - .filter(child -> child.getEntry().getName() - .startsWith(testFile.getName().substring(0, testFile.getName().indexOf(".")))) - .findFirst() - .get().getEntry(); + .getUnfiledContainerChildren(UNFILED_RECORDS_CONTAINER_ALIAS) + .getEntries().stream() + .filter(child -> child.getEntry().getName() + .startsWith(testFile.getName().substring(0, testFile.getName().indexOf(".")))) + .findFirst() + .get().getEntry(); STEP("Delete the record."); deleteAndVerify(unfiledContainerChild.getId()); diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/records/ShareRecordsTest.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/records/ShareRecordsTest.java new file mode 100644 index 0000000000..12495f12b4 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/records/ShareRecordsTest.java @@ -0,0 +1,128 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.rm.community.records; + +import org.alfresco.dataprep.ContentService; +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.rest.v0.RecordsAPI; +import org.alfresco.test.AlfrescoTest; +import org.apache.commons.httpclient.HttpStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.AfterClass; +import org.testng.annotations.Test; + +import static org.alfresco.rest.core.v0.BaseAPI.RM_SITE_ID; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; + +/** + * Tests to cover share action for records + * @author Kavit Shah + */ +public class ShareRecordsTest extends BaseRMRestTest { + + /** data prep services*/ + @Autowired + private RecordsAPI service; + @Autowired + private ContentService contentService; + @Autowired + private RMRolesAndActionsAPI rmRolesAndActionsAPI; + /** Constants*/ + private final String TEST_PREFIX = generateTestPrefix(ShareRecordsTest.class); + private final String CATEGORY = "CategoryWithSharedRecords" + TEST_PREFIX; + private final String FOLDER = "FolderWithSharedRecords" + TEST_PREFIX; + private final String ELECTRONIC_RECORD = "ELECTRONIC_RECORD" + TEST_PREFIX; + private final String NONELECTRONIC_REC = "NON_ELECTRONIC_RECORD" + TEST_PREFIX; + private RecordCategory category; + private RecordCategoryChild recordCategoryChild; + /** + * Given a record + * When admin tries to share it via API + * Then the record can't be shared + */ + @Test + @AlfrescoTest(jira = "RM-5308") + public void shareRecordViaApi() + { + //create RM Site + createRMSiteIfNotExists(); + + //create a category + category = createRootCategory(CATEGORY); + + //create folder + recordCategoryChild = createFolder(category.getId(),FOLDER); + + createNonElectronicRecord(recordCategoryChild.getId(),NONELECTRONIC_REC); + + // create record to be shared + createElectronicRecord(recordCategoryChild.getId(),ELECTRONIC_RECORD); + + //get the node id for the ELECTRONIC_RECORD created + String nodeRefRec1= contentService.getNodeRefByPath(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), + "/Sites/" + RM_SITE_ID + "/documentLibrary/" + CATEGORY + "/" + FOLDER + "/" + service.getRecordFullName(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), FOLDER, ELECTRONIC_RECORD)); + //check record can't be shared + assertFalse("The record has been succesfully shared", + service.shareDocument(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(),nodeRefRec1 ).getKey()); + //check the error code when trying to share a record + assertEquals("The API response code is not " + HttpStatus.SC_INTERNAL_SERVER_ERROR, service.shareDocument(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), nodeRefRec1).getValue(), + String.valueOf( HttpStatus.SC_INTERNAL_SERVER_ERROR)); + + //get the node id for NONELECTRONIC_REC created + String nodeRefRec2 = contentService.getNodeRefByPath(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), + "/Sites/" + RM_SITE_ID + "/documentLibrary/" + CATEGORY + "/" + FOLDER + "/" + service.getRecordFullName(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), FOLDER, NONELECTRONIC_REC)); + //check record can't be shared + assertFalse("The record has been succesfully shared", + service.shareDocument(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), nodeRefRec2).getKey()); + //check the error code when trying to share a record + assertEquals("The API response code is not " + HttpStatus.SC_INTERNAL_SERVER_ERROR, service.shareDocument(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), nodeRefRec2).getValue(), + String.valueOf(HttpStatus.SC_INTERNAL_SERVER_ERROR)); + } + + @AfterClass + public void cleanupCategory() { + rmRolesAndActionsAPI.deleteAllItemsInContainer(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), RM_SITE_ID, recordCategoryChild.getName()); + rmRolesAndActionsAPI.deleteAllItemsInContainer(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), RM_SITE_ID, category.getName()); + deleteRecordCategory(category.getId()); + } +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/AuditAccessTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/AuditAccessTests.java new file mode 100644 index 0000000000..f2ad183915 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/AuditAccessTests.java @@ -0,0 +1,211 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.rm.community.smoke; + +import org.alfresco.dataprep.CMISUtil; +import org.alfresco.rest.core.v0.BaseAPI; +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.audit.AuditEntry; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.rest.v0.RecordsAPI; +import org.alfresco.rest.v0.service.RMAuditService; +import org.alfresco.test.AlfrescoTest; +import org.alfresco.utility.model.UserModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.Test; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import static org.alfresco.rest.core.v0.BaseAPI.NODE_PREFIX; +import static org.alfresco.rest.core.v0.BaseAPI.RM_SITE_ID; +import static org.alfresco.rest.rm.community.model.audit.AuditEvents.DELETE_PERSON; +import static org.alfresco.rest.rm.community.model.audit.AuditEvents.LOGIN_SUCCESSFUL; +import static org.alfresco.rest.rm.community.records.SearchRecordsTests.*; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.junit.Assert.assertFalse; +import static org.testng.AssertJUnit.assertTrue; + +/** + * Audit Access tests + * @author Kavit Shah + */ +public class AuditAccessTests extends BaseRMRestTest { + + private Optional deletedUser; + private final String TEST_PREFIX = generateTestPrefix(AuditAccessTests.class); + private static final String DELETE_USER_EVENT = "Delete User"; + private final String record1 = TEST_PREFIX + "RM-2967 uploaded record"; + private final String classifiedRecord = TEST_PREFIX + "RM-2967 classified record"; + private final String folderName = TEST_PREFIX + "RM-2967 folder"; + private final String categoryName = TEST_PREFIX + "RM-2967 category"; + private final String editedCategoryName = "edited " + categoryName; + private final String editedFolderName = "edited " + folderName; + private final String editedRecordName = "edited " + record1; + private final String login_successfull = "Login Successful"; + private RecordCategory categoryAll; + + @Autowired + private RMRolesAndActionsAPI rmRolesAndActionsAPI; + @Autowired + private RecordsAPI recordsAPI; + @Autowired + private RMAuditService rmAuditService; + + @Test(priority = 1) + @AlfrescoTest(jira = "RM-2967") + public void deleteRMUsersShowFullAuditTest() { + + createTestPrecondition(); + updateCategoryMetadata(); + updateFolderMetadata(); + updateRecordMetadata(); + + // delete record category and folder with rm_admin_deleted + rmRolesAndActionsAPI.deleteAllItemsInContainer(deletedUser.get().getUsername(), deletedUser.get().getPassword(), + RM_SITE_ID, editedFolderName); + rmRolesAndActionsAPI.deleteAllItemsInContainer(deletedUser.get().getUsername(), deletedUser.get().getPassword(), + RM_SITE_ID, editedCategoryName); + + // delete the user + Optional.of(deletedUser).ifPresent(x -> getDataUser().deleteUser(x.get())); + + //check for RM-5235 fix + List auditEntries = rmAuditService.getAuditEntriesFilteredByEvent(getDataUser().usingAdmin().getAdminUser(), + DELETE_PERSON); + + assertTrue("Delete user event not found in the audit log.", auditEntries.stream().anyMatch( + auditEntry -> auditEntry.getEvent().equals(DELETE_USER_EVENT))); + } + + + @Test(priority = 2) + public void filterEventsByLoginSuccessful() + { + createRMSiteIfNotExists(); + List auditEntries = rmAuditService.getAuditEntriesFilteredByEvent(getDataUser().usingAdmin().getAdminUser(), + LOGIN_SUCCESSFUL); + + assertFalse("Audit results should contain at least one Login Successful event", + auditEntries.isEmpty()); + + assertTrue("Audit results contain only Login Successful events", + auditEntries.stream() + .allMatch(e -> e.getEvent().startsWith(LOGIN_SUCCESSFUL.toString()) || e.getEvent().startsWith(login_successfull))); + } + /** + * Creates the required precondition for the test + *

+ * See Precondition in current class JavaDoc + */ + private void createTestPrecondition() { + createRMSiteIfNotExists(); + + // create "rm deleted user" user if it does not exist and assign it to RM Administrator role + createDeletedUser(); + + // create category and folder + categoryAll = createCategoryIfDoesNotExist(categoryName,deletedUser.get()); + createRecordFolderInCategory(folderName,categoryAll,deletedUser.get()); + // upload an electronic record + + recordsAPI.uploadElectronicRecord(deletedUser.get().getUsername(), deletedUser.get().getPassword(), getDefaultElectronicRecordProperties(record1), folderName, CMISUtil.DocumentType.TEXT_PLAIN); + // upload another electronic record and classify it + recordsAPI.uploadElectronicRecord(deletedUser.get().getUsername(), deletedUser.get().getPassword(), getDefaultElectronicRecordProperties(classifiedRecord), folderName, CMISUtil.DocumentType.TEXT_PLAIN); + } + + private void createDeletedUser() { + // create Deleted User + deletedUser = Optional.ofNullable(getDataUser().createRandomTestUser()); + rmRolesAndActionsAPI.assignRoleToUser( + getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), + deletedUser.get().getUsername(), + ADMIN + ); + } + + private void updateCategoryMetadata() { + HashMap categoryProperties = new HashMap<>(); + categoryProperties.put(BaseAPI.RMProperty.NAME, editedCategoryName); + categoryProperties.put(BaseAPI.RMProperty.TITLE, "edited " + TITLE); + categoryProperties.put(BaseAPI.RMProperty.DESCRIPTION, "edited " + DESCRIPTION); + + // edit some category's properties + String categoryNodeRef = NODE_PREFIX + rmRolesAndActionsAPI.getItemNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), "/" + categoryName); + rmRolesAndActionsAPI.updateMetadata(deletedUser.get().getUsername(), deletedUser.get().getPassword(), categoryNodeRef, categoryProperties); + } + + private void updateFolderMetadata() { + HashMap folderProperties = new HashMap<>(); + folderProperties.put(BaseAPI.RMProperty.NAME, editedFolderName); + folderProperties.put(BaseAPI.RMProperty.TITLE, "edited " + TITLE); + folderProperties.put(BaseAPI.RMProperty.DESCRIPTION, "edited " + DESCRIPTION); + + // edit some folder's properties + String folderNodeRef = NODE_PREFIX + rmRolesAndActionsAPI.getItemNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), "/" + editedCategoryName + "/" + folderName); + rmRolesAndActionsAPI.updateMetadata(deletedUser.get().getUsername(), deletedUser.get().getPassword(), folderNodeRef, folderProperties); + } + + private void updateRecordMetadata() { + HashMap recordProperties = new HashMap<>(); + recordProperties.put(BaseAPI.RMProperty.NAME, editedRecordName); + recordProperties.put(BaseAPI.RMProperty.TITLE, "edited " + TITLE); + recordProperties.put(BaseAPI.RMProperty.AUTHOR, "edited author"); + recordProperties.put(BaseAPI.RMProperty.DESCRIPTION, "edited " + DESCRIPTION); + + // edit some record's properties + String recordName = recordsAPI.getRecordFullName(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), editedFolderName, record1); + String recordNodeRef = NODE_PREFIX + rmRolesAndActionsAPI.getItemNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), "/" + editedCategoryName + "/" + editedFolderName + "/" + recordName); + rmRolesAndActionsAPI.updateMetadata(deletedUser.get().getUsername(), deletedUser.get().getPassword(), recordNodeRef, recordProperties); + } + + + private RecordCategory createCategoryIfDoesNotExist(String CATEGORY_ALL, UserModel deletedUser) { + return createRootCategory(deletedUser, CATEGORY_ALL); + } + + private RecordCategoryChild createRecordFolderInCategory(String FOLDER_SEARCH, RecordCategory recordCategory, UserModel deletedUser) { + return createFolder(deletedUser, recordCategory.getId(), FOLDER_SEARCH); + } + + private Map getDefaultElectronicRecordProperties(String recordName) { + Map defaultProperties = new HashMap<>(); + defaultProperties.put(BaseAPI.RMProperty.NAME, recordName); + defaultProperties.put(BaseAPI.RMProperty.TITLE, TITLE); + defaultProperties.put(BaseAPI.RMProperty.DESCRIPTION, DESCRIPTION); + defaultProperties.put(BaseAPI.RMProperty.CONTENT, TEST_CONTENT); + return defaultProperties; + } +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/BasicRulesIntegrationTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/BasicRulesIntegrationTests.java new file mode 100644 index 0000000000..b8224a31e9 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/BasicRulesIntegrationTests.java @@ -0,0 +1,104 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rm.community.smoke; +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.record.Record; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.rules.ActionsOnRule; +import org.alfresco.rest.rm.community.model.rules.RuleDefinition; +import org.alfresco.rest.rm.community.requests.gscore.api.RecordFolderAPI; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.rest.v0.RulesAPI; +import org.alfresco.test.AlfrescoTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.Test; + + +import java.util.Collections; +import static org.alfresco.rest.core.v0.BaseAPI.NODE_PREFIX; +import static org.alfresco.rest.rm.community.base.TestData.ELECTRONIC_RECORD_NAME; +import static org.alfresco.rest.rm.community.base.TestData.NONELECTRONIC_RECORD_NAME; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.rest.rm.community.utils.FilePlanComponentsUtil.*; +import static org.alfresco.utility.data.RandomData.getRandomName; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.springframework.http.HttpStatus.*; +public class BasicRulesIntegrationTests extends BaseRMRestTest { + + @Autowired + private RMRolesAndActionsAPI rmRolesAndActionsAPI; + private final static String title = "Rule to complete"; + private final static String description = "Rule to describe"; + private final String TEST_PREFIX = generateTestPrefix(CreateCategoriesTests.class); + private final String RM_ADMIN = TEST_PREFIX + "rm_admin"; + @Autowired + private RulesAPI rulesAPI; + + @Test + @AlfrescoTest(jira = "RM-2794") + public void basicRulesIntegration() { + + + STEP("Create the RM site if doesn't exist"); + createRMSiteIfNotExists(); + + STEP("Create RM Admin user"); + rmRolesAndActionsAPI.createUserAndAssignToRole(getAdminUser().getUsername(), getAdminUser().getPassword(), RM_ADMIN, + getAdminUser().getPassword(), + "Administrator"); + + STEP("Create record categories and record folders"); + RecordCategory Category = createRootCategory(getRandomName("recordCategory")); + String recordFolder1 = createRecordFolder(Category.getId(), getRandomName("recFolder")).getId(); + + + //create a rule for completing a record + RuleDefinition ruleDefinition = RuleDefinition.createNewRule().title("name").description("description1") + .applyToChildren(true).title(title) + .actions(Collections.singletonList(ActionsOnRule.COMPLETE_RECORD.getActionValue())); + rulesAPI.createRule(getAdminUser().getUsername(), getAdminUser().getPassword(), NODE_PREFIX + Category.getId(), ruleDefinition); + + RecordFolderAPI recordFolderAPI = getRestAPIFactory().getRecordFolderAPI(); + + //create two electronic record in record folder + String electronicRecordId1 = createElectronicRecord(recordFolder1, ELECTRONIC_RECORD_NAME).getId(); + String electronicRecordId2 = createElectronicRecord(recordFolder1, ELECTRONIC_RECORD_NAME).getId(); + assertStatusCode(CREATED); + + + + // Update the rules for record Category + rulesAPI.updateRule(getAdminUser().getUsername(), getAdminUser().getPassword(), + NODE_PREFIX + Category.getId(), ruleDefinition.description("description").id(description)); + + //Delete the root category and rules + deleteRecordCategory(Category.getId()); + rulesAPI.deleteAllRulesOnContainer(getAdminUser().getUsername(), getAdminUser().getPassword(), NODE_PREFIX + Category.getId()); + } + + +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/CreateCategoriesTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/CreateCategoriesTests.java new file mode 100644 index 0000000000..38a0e111ad --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/CreateCategoriesTests.java @@ -0,0 +1,125 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.rm.community.smoke; + +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.fileplan.FilePlan; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.test.AlfrescoTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAlias.FILE_PLAN_ALIAS; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.rest.rm.community.utils.CoreUtil.createBodyForMoveCopy; +import static org.alfresco.rest.rm.community.utils.CoreUtil.toContentModel; +import static org.alfresco.utility.data.RandomData.getRandomAlphanumeric; +import static org.alfresco.utility.data.RandomData.getRandomName; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.junit.Assert.assertFalse; +import static org.springframework.http.HttpStatus.OK; +import static org.testng.Assert.assertEquals; + +public class CreateCategoriesTests extends BaseRMRestTest { + + @Autowired + private RMRolesAndActionsAPI rmRolesAndActionsAPI; + private RecordCategory rootCategory; + private final String TEST_PREFIX = generateTestPrefix(CreateCategoriesTests.class); + private final String RM_ADMIN = TEST_PREFIX + "rm_admin"; + private RecordCategory Category1; + private RecordCategory Category2; + private RecordCategory SubCategory1; + private RecordCategory SubCategory2; + + @BeforeClass(alwaysRun = true) + public void preconditionForCreateCategoriesTests() + { + STEP("Create the RM site if doesn't exist"); + createRMSiteIfNotExists(); + + STEP("Create RM Admin user"); + rmRolesAndActionsAPI.createUserAndAssignToRole(getAdminUser().getUsername(), getAdminUser().getPassword(), RM_ADMIN, + getAdminUser().getPassword(), + "Administrator"); + + STEP("Create two category"); + Category1 = createRootCategory(getRandomName("Category1")); + + Category2= createRootCategory(getRandomName("Category2")); + + STEP("Create Sub category"); + RecordCategoryChild subCategory1 = createRecordCategory(Category1.getId(), getRandomName("subCategory1")); + RecordCategoryChild subCategory2 = createRecordCategory(Category2.getId(), getRandomName("subCategory2")); + + } + + + @Test @AlfrescoTest(jira = "RM-2756") + public void createCategories() throws Exception { + + FilePlan filePlan = getRestAPIFactory().getFilePlansAPI().getFilePlan(FILE_PLAN_ALIAS); + + STEP("copy category 1 to File Plan."); + getRestAPIFactory().getNodeAPI(toContentModel(Category1.getId())).copy(createBodyForMoveCopy(filePlan.getId())); + + STEP("copy category 1 to category 2"); + getRestAPIFactory().getNodeAPI(toContentModel(Category1.getId())).copy(createBodyForMoveCopy(Category2.getId())); + + String categoryName = "Category name " + getRandomAlphanumeric(); + String categoryTitle = "Category title " + getRandomAlphanumeric(); + + + // Create the root record category + RecordCategory Category1 = createRootCategory(categoryName, categoryTitle); + + String newCategoryName = "Rename " + categoryName; + + // Build the properties which will be updated + RecordCategory recordCategoryUpdated = Category1.builder().name(newCategoryName).build(); + + // Update the record category + RecordCategory renamedRecordCategory = getRestAPIFactory().getRecordCategoryAPI().updateRecordCategory(recordCategoryUpdated,Category1.getId()); + // Verify the status code + assertStatusCode(OK); + + // verify renamed component and editTitle component still has this parent + assertEquals(renamedRecordCategory.getParentId(), filePlan.getId()); + + STEP("move category 1 edited copy to File Plan"); + getRestAPIFactory().getNodeAPI(toContentModel(renamedRecordCategory.getId())).move(createBodyForMoveCopy(filePlan.getId())); + assertStatusCode(OK); + + // delete All the categories + deleteRecordCategory(Category1.getId()); + deleteRecordCategory(Category2.getId()); + } +} \ No newline at end of file diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/CreateFoldersTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/CreateFoldersTests.java new file mode 100644 index 0000000000..35335584d9 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/CreateFoldersTests.java @@ -0,0 +1,147 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rm.community.smoke; + +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.common.ReviewPeriod; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.rm.community.model.recordfolder.RecordFolder; +import org.alfresco.rest.rm.community.model.recordfolder.RecordFolderProperties; +import org.alfresco.rest.rm.community.requests.gscore.api.RecordCategoryAPI; +import org.alfresco.rest.rm.community.requests.gscore.api.RecordFolderAPI; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.test.AlfrescoTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.rest.rm.community.utils.CoreUtil.createBodyForMoveCopy; +import static org.alfresco.rest.rm.community.utils.CoreUtil.toContentModel; +import static org.alfresco.utility.data.RandomData.getRandomAlphanumeric; +import static org.alfresco.utility.data.RandomData.getRandomName; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.springframework.http.HttpStatus.*; + +public class CreateFoldersTests extends BaseRMRestTest { + + @Autowired + private RMRolesAndActionsAPI rmRolesAndActionsAPI; + + private final String TEST_PREFIX = generateTestPrefix(CreateCategoriesTests.class); + private final String RM_ADMIN = TEST_PREFIX + "rm_admin"; + private RecordCategory Category1; + private RecordCategory Category2; + private RecordCategoryChild recordCategoryChild; + + @BeforeClass(alwaysRun = true) + public void preconditionForCreateFolderTests() { + STEP("Create the RM site if doesn't exist"); + createRMSiteIfNotExists(); + + STEP("Create RM Admin user"); + rmRolesAndActionsAPI.createUserAndAssignToRole(getAdminUser().getUsername(), getAdminUser().getPassword(), RM_ADMIN, + getAdminUser().getPassword(), + "Administrator"); + + STEP("Create two category"); + Category1 = createRootCategory(getRandomName("Category1")); + + Category2 = createRootCategory(getRandomName("Category2")); + + // Create a record folder inside the category 1 + recordCategoryChild = createRecordFolder(Category1.getId(), getRandomName("recFolder")); + + } + + @Test + @AlfrescoTest(jira = "RM-2757") + public void createFolders() throws Exception { + + // Create record category first + String folderDescription = "The folder description is updated" + getRandomAlphanumeric(); + String folderName = "The folder name is updated" + getRandomAlphanumeric(); + String folderTitle = "Update title " + getRandomAlphanumeric(); + String location = "Location "+ getRandomAlphanumeric(); + + // Create the record folder properties to update + RecordFolder recordFolder = RecordFolder.builder() + .name(folderName) + .properties(RecordFolderProperties.builder() + .title(folderTitle) + .description(folderDescription) + .vitalRecordIndicator(true) + .reviewPeriod(new ReviewPeriod("month","1")) + .location(location) + .build()) + .build(); + + // Update the record folder + RecordFolder updatedRecordFolder = getRestAPIFactory().getRecordFolderAPI().updateRecordFolder(recordFolder, recordCategoryChild.getId()); + + // Check the Response Status Code + assertStatusCode(OK); + + STEP("copy updated Record in category 1 and category 2"); + getRestAPIFactory().getNodeAPI(toContentModel(updatedRecordFolder.getId())).copy(createBodyForMoveCopy(Category1.getId())); + //assertStatusCode(OK); + getRestAPIFactory().getNodeAPI(toContentModel(updatedRecordFolder.getId())).copy(createBodyForMoveCopy(Category2.getId())); + //assertStatusCode(OK); + + + // Delete the Updated folder + RecordFolderAPI recordFolderAPI = getRestAPIFactory().getRecordFolderAPI(); + String recordFolderId = updatedRecordFolder.getId(); + recordFolderAPI.deleteRecordFolder(recordFolderId); + + // Check the response status code + assertStatusCode(NO_CONTENT); + + // Check the record folder is not found + recordFolderAPI.getRecordFolder(recordFolderId); + + // Check the response status code + assertStatusCode(NOT_FOUND); + + STEP("move updated Record from category 1 to category 2"); + getRestAPIFactory().getNodeAPI(toContentModel(updatedRecordFolder.getId())).move(createBodyForMoveCopy(Category2.getId())); + + // move category 2 to category 1 + getRestAPIFactory().getNodeAPI(toContentModel(Category2.getId())).move(createBodyForMoveCopy(Category1.getId())); + + // Delete the record category + RecordCategoryAPI recordCategoryAPI = getRestAPIFactory().getRecordCategoryAPI(); + String recordCategoryId = Category1.getId(); + recordCategoryAPI.deleteRecordCategory(recordCategoryId); + + // Verify the status code + assertStatusCode(NO_CONTENT); + + } + +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/DeclareDocsAsRecordsOnUpdateRuleNewVersionTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/DeclareDocsAsRecordsOnUpdateRuleNewVersionTests.java new file mode 100644 index 0000000000..f4bcd7a7b5 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/DeclareDocsAsRecordsOnUpdateRuleNewVersionTests.java @@ -0,0 +1,122 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rm.community.smoke; + +import lombok.Getter; +import org.alfresco.dataprep.CMISUtil; +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.rm.community.model.rules.ActionsOnRule; +import org.alfresco.rest.rm.community.model.rules.RuleDefinition; +import org.alfresco.rest.rm.community.model.unfiledcontainer.UnfiledContainerChildEntry; +import org.alfresco.rest.rm.community.requests.gscore.api.UnfiledContainerAPI; +import org.alfresco.rest.v0.RulesAPI; +import org.alfresco.test.AlfrescoTest; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.data.DataUserAIS; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static lombok.AccessLevel.PROTECTED; +import static org.alfresco.rest.core.v0.BaseAPI.NODE_PREFIX; +import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAlias.UNFILED_RECORDS_CONTAINER_ALIAS; +import static org.alfresco.utility.data.RandomData.getRandomName; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.springframework.http.HttpStatus.CREATED; + +public class DeclareDocsAsRecordsOnUpdateRuleNewVersionTests extends BaseRMRestTest { + + + @Autowired + private DataSite dataSite; + private SiteModel publicSite; + private RecordCategory recordCategory; + @Autowired + private RulesAPI rulesAPI; + @Autowired + protected DataContent dataContent; + @Autowired + @Getter(value = PROTECTED) + protected DataUserAIS dataUser; + private final static String title = "Rule to convert document as record"; + + @BeforeClass (alwaysRun = true) + public void setUp() + { + publicSite = dataSite.usingAdmin().createPublicRandomSite(); + recordCategory = createRootCategory(getRandomName("recordCategory")); + + } + + @Test + @AlfrescoTest(jira = "RM-1521") + public void declareDocsAsRecordsOnUpdateRuleNewVersion() { + FolderModel testFolder; + + STEP("Create test collaboration site to store documents in."); + publicSite = dataSite.usingAdmin().createPublicRandomSite(); + + STEP("Create a record folder with a DECLARE_AS_RECORD"); + RecordCategoryChild folderWithRule = createFolder(recordCategory.getId(), getRandomName("recordFolder")); + RuleDefinition ruleDefinition = RuleDefinition.createNewRule().title("name").description("description") + .applyToChildren(true) + .actions(Collections.singletonList(ActionsOnRule.DECLARE_AS_RECORD.getActionValue())); + rulesAPI.createRule(getAdminUser().getUsername(), getAdminUser().getPassword(), NODE_PREFIX + folderWithRule.getId(), ruleDefinition); + + STEP("Create a document in the collaboration site"); + FileModel testFile = dataContent.usingSite(publicSite) + .usingAdmin() + .createContent(CMISUtil.DocumentType.TEXT_PLAIN); + assertStatusCode(CREATED); + + + // verify the declared record is in Unfilled Records folder + UnfiledContainerAPI unfiledContainersAPI = getRestAPIFactory().getUnfiledContainersAPI(); + List matchingRecords = unfiledContainersAPI.getUnfiledContainerChildren(UNFILED_RECORDS_CONTAINER_ALIAS) + .getEntries() + .stream() + .filter(e -> e.getEntry().getId().equals(testFile.getNodeRefWithoutVersion())) + .collect(Collectors.toList()); + + //delete rm items + deleteRecordCategory(recordCategory.getId()); + STEP("Delete the record."); + //delete created collaboration site + dataSite.deleteSite(publicSite); + + } +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/DestroyRecordFolderActionsTest.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/DestroyRecordFolderActionsTest.java new file mode 100644 index 0000000000..a3f35e5423 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/DestroyRecordFolderActionsTest.java @@ -0,0 +1,121 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rm.community.smoke; + +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.rest.v0.RecordFoldersAPI; +import org.alfresco.rest.v0.service.DispositionScheduleService; +import org.alfresco.test.AlfrescoTest; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static org.alfresco.rest.rm.community.model.recordcategory.RetentionPeriodProperty.CREATED_DATE; +import static org.alfresco.rest.rm.community.model.recordcategory.RetentionPeriodProperty.CUT_OFF_DATE; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.rest.rm.community.utils.CoreUtil.createBodyForMoveCopy; +import static org.alfresco.rest.rm.community.utils.CoreUtil.toContentModel; +import static org.alfresco.utility.data.RandomData.getRandomName; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.springframework.http.HttpStatus.OK; +import static org.testng.AssertJUnit.assertNotNull; + + +public class DestroyRecordFolderActionsTest extends BaseRMRestTest { + + private RecordCategory Category1,CATEGORY_TO_MOVE; + @Autowired + private DispositionScheduleService dispositionScheduleService; + @Autowired + private RecordFoldersAPI recordFoldersAPI; + private final String TEST_PREFIX = generateTestPrefix(DestroyRecordFolderActionsTest.class); + private final String folderDisposition = TEST_PREFIX + "RM-2937 folder ghosting"; + + + @BeforeClass(alwaysRun = true) + private void setUp(){ + + STEP("Create the RM site if doesn't exist"); + createRMSiteIfNotExists(); + + STEP("Create two record category"); + Category1 = createRootCategory(getRandomName("Category1")); + CATEGORY_TO_MOVE = createRootCategory(getRandomName("CATEGORY_TO_MOVE")); + + //create retention schedule + dispositionScheduleService.createCategoryRetentionSchedule(Category1.getName(), false); + + // add cut off step + dispositionScheduleService.addCutOffAfterPeriodStep(Category1.getName(), "day|2", CREATED_DATE); + + // add destroy step with ghosting + dispositionScheduleService.addDestroyWithGhostingImmediatelyAfterCutOff(Category1.getName()); + + } + + @Test + @AlfrescoTest (jira = "RM-1621") + public void moveOnCutOffDestroyFolders() throws Exception { + + //create folders + RecordCategoryChild FOLDER_DESTROY = createFolder(getAdminUser(),Category1.getId(),folderDisposition); + + // edit disposition date + recordFoldersAPI.postFolderAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),editDispositionDateJson(),FOLDER_DESTROY.getName()); + + // cut off the FOLDER_DESTROY + recordFoldersAPI.postFolderAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","cutoff"),FOLDER_DESTROY.getName()); + + + // Destroy the FOLDER_DESTROY + recordFoldersAPI.postFolderAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","destroy"),FOLDER_DESTROY.getName()); + + + //Move the FOLDER_DESTROY within the CATEGORY_TO_MOVE."); + getRestAPIFactory().getNodeAPI(toContentModel(FOLDER_DESTROY.getId())).move(createBodyForMoveCopy(CATEGORY_TO_MOVE.getId())); + assertStatusCode(OK); + + } + + @AfterMethod(alwaysRun = true) + private void deletePreconditions() { + + deleteRecordCategory(Category1.getId()); + deleteRecordCategory(CATEGORY_TO_MOVE.getId()); + + } + + } diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/DispositionScheduleLinkedRecordsTest.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/DispositionScheduleLinkedRecordsTest.java new file mode 100644 index 0000000000..b1badc40c5 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/DispositionScheduleLinkedRecordsTest.java @@ -0,0 +1,425 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rm.community.smoke; + +import org.alfresco.rest.core.v0.RMEvents; +import org.alfresco.rest.model.RestNodeBodyMoveCopyModel; +import org.alfresco.rest.model.RestNodeModel; +import org.alfresco.rest.requests.Node; +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.fileplan.FilePlan; +import org.alfresco.rest.rm.community.model.record.Record; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.rm.community.model.user.UserRoles; +import org.alfresco.rest.v0.LinksAPI; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.rest.v0.RecordFoldersAPI; +import org.alfresco.rest.v0.RecordsAPI; +import org.alfresco.rest.v0.service.DispositionScheduleService; +import org.alfresco.test.AlfrescoTest; +import org.alfresco.utility.model.RepoTestModel; +import org.alfresco.utility.model.UserModel; +import org.apache.commons.lang.StringUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.util.EntityUtils; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.AssertJUnit; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import static org.alfresco.rest.core.v0.BaseAPI.NODE_REF_WORKSPACE_SPACES_STORE; +import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAlias.FILE_PLAN_ALIAS; +import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAspects.CUT_OFF_ASPECT; +import static org.alfresco.rest.rm.community.model.recordcategory.RetentionPeriodProperty.*; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.utility.data.RandomData.getRandomName; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.http.HttpStatus.NO_CONTENT; + +public class DispositionScheduleLinkedRecordsTest extends BaseRMRestTest { + @Autowired + private RMRolesAndActionsAPI rmRolesAndActionsAPI; + @Autowired + private DispositionScheduleService dispositionScheduleService; + @Autowired + private LinksAPI linksAPI; + @Autowired + private RecordsAPI recordsAPI; + @Autowired + private RecordFoldersAPI recordFoldersAPI; + private final static String TEST_PREFIX = generateTestPrefix(DispositionScheduleLinkedRecordsTest.class); + private RecordCategory Category1,catsameLevel1,catsameLevel2; + private RecordCategoryChild CopyCatFolder,folder1,CatFolder,folder2; + private static final String categoryRM3077 = TEST_PREFIX + "RM-3077_manager_sees_me"; + private static final String copyCategoryRM3077 = "Copy_of_" + categoryRM3077; + private static final String folderRM3077 = "RM-3077_folder_"+ categoryRM3077; + private static final String copyFolderRM3077 = "Copy_of_" + folderRM3077; + private final String electronicRecord = "RM-2937 electronic 2 record"; + private final String folder = TEST_PREFIX + "RM-2937 folder ghosting"; + private static final String categoryRecordsRM2526 = TEST_PREFIX + "RM-2526_category_records_immediately"; + private static final String category2RecordsRM2526 = TEST_PREFIX + "RM-2526_category_2_records_1_day"; + private static final String firstCategoryRM3060 = TEST_PREFIX + "RM-3060_category_record"; + private static final String secondCategoryRM3060 = "Copy_of_" + firstCategoryRM3060; + private static final String firstFolderRM3060 = TEST_PREFIX + "RM-3060_folder"; + private static final String secondFolderRM3060 = TEST_PREFIX + "RM-3060_disposition_on_Record_Level"; + private static final String electronicRecordRM3060 = TEST_PREFIX + "RM-3060_electronic_1_record"; + private static final String nonElectronicRecordRM3060 = TEST_PREFIX + "RM-3060_non-electronic_record"; + private static final String TRANSFER_LOCATION = TEST_PREFIX + "RM-3060_transferred_records"; + public static final String TRANSFER_TYPE = "rma:transferred"; + private FilePlan filePlanModel; + private UserModel rmAdmin, rmManager; + @BeforeClass(alwaysRun = true) + public void setupDispositionScheduleLinkedRecordsTest() { + createRMSiteIfNotExists(); + //get file plan + filePlanModel = getRestAPIFactory().getFilePlansAPI().getFilePlan(FILE_PLAN_ALIAS); + + // create "rm admin" user if it does not exist and assign it to RM Administrator role + rmAdmin = getDataUser().createRandomTestUser(); + rmRolesAndActionsAPI.assignRoleToUser(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(),rmAdmin.getUsername(), + UserRoles.ROLE_RM_ADMIN.roleId); + + // create "rm Manager" user if it does not exist and assign it to RM Administrator role + rmManager = getDataUser().createRandomTestUser(); + rmRolesAndActionsAPI.assignRoleToUser(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(),rmManager.getUsername(), + UserRoles.ROLE_RM_MANAGER.roleId); + } + /** + * Disposition Schedule on Record Folder with linked records test + *

+ * Precondition: + *

+ * Create rm_manager user that would have RM Managers role, rm_admin that would have RM Administrator role. + * Log in with admin user, create a category "manager sees me", give rm_manager read&file permission over it. + * Create a disposition schedule for it that would cut off folders after 1 day from created date. Copy the category. + *

+ *

TestRail Test C775

+ **/ + @Test + @AlfrescoTest(jira = "RM-1622") + public void dispositionScheduleLinkedRecords() throws UnsupportedEncodingException { + STEP("Create record category"); + Category1 = createRootCategory(categoryRM3077); + + //create retention schedule + dispositionScheduleService.createCategoryRetentionSchedule(Category1.getName(), false); + + // add cut off step + dispositionScheduleService.addCutOffAfterPeriodStep(Category1.getName(), "day|2", CREATED_DATE); + + //create a copy of the category recordsCategory + String CopyCategoryId = copyCategory(getAdminUser(),Category1.getId(), copyCategoryRM3077); + + // create folders in both categories + CatFolder = createRecordFolder(Category1.getId(), folderRM3077); + CopyCatFolder = createRecordFolder(CopyCategoryId, copyFolderRM3077); + + // create record files + String electronicRecord = "RM-2801 electronic record"; + Record elRecord = createElectronicRecord(CatFolder.getId(), electronicRecord); + String elRecordFullName = recordsAPI.getRecordFullName(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), CatFolder.getName(), electronicRecord); + + String nonElectronicRecord = "RM-2801 non-electronic record"; + Record nonElRecord = createNonElectronicRecord(CatFolder.getId(), nonElectronicRecord); + String nonElRecordFullName = recordsAPI.getRecordFullName(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), CatFolder.getName(), nonElectronicRecord); + + // link the records to copy folder, then complete them + List recordLists = new ArrayList<>(); + recordLists.add(NODE_REF_WORKSPACE_SPACES_STORE + elRecord.getId()); + recordLists.add(NODE_REF_WORKSPACE_SPACES_STORE + nonElRecord.getId()); + + linksAPI.linkRecord(getDataUser().getAdminUser().getUsername(), + getDataUser().getAdminUser().getPassword(), HttpStatus.SC_OK,copyCategoryRM3077 + "/" + + copyFolderRM3077, recordLists); + recordsAPI.completeRecord(rmAdmin.getUsername(), rmAdmin.getPassword(), elRecordFullName); + recordsAPI.completeRecord(rmAdmin.getUsername(), rmAdmin.getPassword(), nonElRecordFullName); + + // edit disposition date + recordFoldersAPI.postFolderAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),editDispositionDateJson(),CatFolder.getName()); + + // cut off the Folder + recordFoldersAPI.postFolderAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","cutoff"),CatFolder.getName()); + + // Verify the Content + Node electronicNode = getNode(elRecord.getId()); + assertTrue("The content of " + electronicRecord + " is available", + StringUtils.isEmpty(electronicNode.getNodeContent().getResponse().getBody().asString())); + + // verify the Properties + AssertJUnit.assertNull("The properties are present even after cutting off the record.", elRecord.getProperties().getTitle()); + + // delete precondition + deleteRecordCategory(Category1.getId()); + deleteRecordCategory(CopyCategoryId); + } + /** + * Test covering RM-3060 + * Check the disposition steps for a record can be executed + * When the record is linked to a folder with the same disposition schedule + * */ + @Test + @AlfrescoTest (jira = "RM-3060") + public void sameDispositionScheduleLinkedRecords() throws UnsupportedEncodingException { + + // create a category with retention applied on records level + RecordCategory recordCategory = getRestAPIFactory().getFilePlansAPI(rmAdmin) + .createRootRecordCategory(RecordCategory.builder().name(firstCategoryRM3060).build(), + RecordCategory.DEFAULT_FILE_PLAN_ALIAS); + dispositionScheduleService.createCategoryRetentionSchedule(firstCategoryRM3060, true); + dispositionScheduleService.addCutOffAfterPeriodStep(firstCategoryRM3060, "week|1", DATE_FILED); + dispositionScheduleService.addTransferAfterEventStep(firstCategoryRM3060, TRANSFER_LOCATION, RMEvents.CASE_CLOSED.getEventName()); + dispositionScheduleService.addDestroyWithoutGhostingAfterPeriodStep(firstCategoryRM3060, "week|1", CUT_OFF_DATE); + + // make a copy of the category created + String categorySecondId = copyCategory(getAdminUser(), recordCategory.getId(), secondCategoryRM3060); + + // create a folder on the category firstCategoryRM3060 with a complete electronic record + RecordCategoryChild firstFolderRecordCategoryChild = createRecordFolder(recordCategory.getId(),firstFolderRM3060); + Record firstElectronicRecord = createElectronicRecord(firstFolderRecordCategoryChild.getId(),electronicRecordRM3060); + + String elRecordFullName = recordsAPI.getRecordFullName(getDataUser().getAdminUser().getUsername(), + getDataUser().getAdminUser().getPassword(),firstFolderRM3060, electronicRecordRM3060); + String elRecordNameNodeRef = recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), elRecordFullName, "/" + firstCategoryRM3060 + "/" + firstFolderRM3060); + + recordsAPI.completeRecord(getDataUser().getAdminUser().getUsername(), + getDataUser().getAdminUser().getPassword(), elRecordFullName); + + // create a folder on the category secondCategoryRM3060 with a non electronic record + RecordCategoryChild secondFolderRecordCategoryChild = createRecordFolder(categorySecondId,secondFolderRM3060); + Record secondNonElectronicRecord = createNonElectronicRecord(secondFolderRecordCategoryChild.getId(),nonElectronicRecordRM3060); + + // link the nonElectronicRecordRM3060 to firstFolderRM3060 + List recordLists = new ArrayList<>(); + recordLists.add(NODE_REF_WORKSPACE_SPACES_STORE + secondNonElectronicRecord.getId()); + + linksAPI.linkRecord(getDataUser().getAdminUser().getUsername(), + getDataUser().getAdminUser().getPassword(), HttpStatus.SC_OK,secondCategoryRM3060 + "/" + + secondFolderRM3060, recordLists); + String nonElRecordFullName = recordsAPI.getRecordFullName(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), secondFolderRM3060, secondNonElectronicRecord.getName()); + String nonElRecordNameNodeRef = recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), nonElRecordFullName, "/" + secondCategoryRM3060 + "/" + secondFolderRM3060); + + // complete records and cut them off + recordsAPI.completeRecord(getDataUser().getAdminUser().getUsername(), + getDataUser().getAdminUser().getPassword(), nonElRecordFullName); + + // edit the disposition date + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),editDispositionDateJson(),nonElRecordNameNodeRef); + + // cut off the record + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","cutoff"),nonElRecordNameNodeRef); + + //check the record is cut off + AssertJUnit.assertTrue("The file " + nonElectronicRecordRM3060 + " has not been successfully cut off.", getRestAPIFactory().getRecordsAPI().getRecord(secondNonElectronicRecord.getId()).getAspectNames().contains(CUT_OFF_ASPECT)); + + // link the electronic record to secondFolderRM3060 + recordLists.clear(); + recordLists.add(NODE_REF_WORKSPACE_SPACES_STORE + secondNonElectronicRecord.getId()); + linksAPI.linkRecord(getDataUser().getAdminUser().getUsername(), + getDataUser().getAdminUser().getPassword(), HttpStatus.SC_OK,secondCategoryRM3060 + "/" + + secondFolderRM3060, recordLists); + + // edit the disposition date and cut off the record + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),editDispositionDateJson(),elRecordNameNodeRef); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","cutoff"),elRecordNameNodeRef); + + AssertJUnit.assertTrue("The file " + electronicRecordRM3060 + " has not been successfully cut off.", getRestAPIFactory().getRecordsAPI().getRecord(firstElectronicRecord.getId()).getAspectNames().contains(CUT_OFF_ASPECT)); + + // open the record and complete the disposition schedule event + rmRolesAndActionsAPI.completeEvent(getAdminUser().getUsername(), + getAdminUser().getPassword(), elRecordFullName, RMEvents.CASE_CLOSED, Instant.now()); + rmRolesAndActionsAPI.completeEvent(getAdminUser().getUsername(), + getAdminUser().getPassword(), nonElRecordFullName, RMEvents.CASE_CLOSED, Instant.now()); + + // transfer the files & complete transfers + HttpResponse nonElRecordNameHttpResponse = recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","transfer"),recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), nonElRecordFullName, "/" + secondCategoryRM3060 + "/" + secondFolderRM3060)); + + String nonElRecordNameTransferId = getTransferId(nonElRecordNameHttpResponse,nonElRecordNameNodeRef); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","transferComplete"),nonElRecordNameTransferId); + + HttpResponse elRecordNameHttpResponse = recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","transfer"),recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), elRecordFullName, "/" + firstCategoryRM3060 + "/" + firstFolderRM3060)); + + String elRecordNameTransferId = getTransferId(elRecordNameHttpResponse,elRecordNameNodeRef); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","transferComplete"),elRecordNameTransferId); + + AssertJUnit.assertTrue("The file " + electronicRecordRM3060 + " has not been successfully transferred", getRestAPIFactory().getRecordsAPI().getRecord(firstElectronicRecord.getId()).getAspectNames().contains(TRANSFER_TYPE)); + AssertJUnit.assertTrue("The file " + nonElectronicRecordRM3060 + " has not been successfully transferred.", getRestAPIFactory().getRecordsAPI().getRecord(secondNonElectronicRecord.getId()).getAspectNames().contains(TRANSFER_TYPE)); + + // edit the disposition date for nonElectronicRecordRM3060 & electronicRecordRM3060 + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),editDispositionDateJson(),nonElRecordNameNodeRef); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),editDispositionDateJson(),elRecordNameNodeRef); + + // destroy nonElectronicRecordRM3060 & electronicRecordRM3060 records + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","destroy"),nonElRecordNameNodeRef); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","destroy"),elRecordNameNodeRef); + + // check the file is not displayed + assertNull("The file " + nonElectronicRecordRM3060 + " has not been successfully destroyed.", secondNonElectronicRecord.getContent()); + assertNull("The file " + electronicRecordRM3060 + " has not been successfully destroyed.", firstElectronicRecord.getContent()); + + // delete precondition + deleteRecordCategory(recordCategory.getId()); + deleteRecordCategory(categorySecondId); + } + private String copyCategory(UserModel user, String categoryId, String copyName) { + RepoTestModel repoTestModel = new RepoTestModel() {}; + repoTestModel.setNodeRef(categoryId); + RestNodeModel restNodeModel; + + RestNodeBodyMoveCopyModel copyDestinationInfo = new RestNodeBodyMoveCopyModel(); + copyDestinationInfo.setTargetParentId(filePlanModel.getId()); + copyDestinationInfo.setName(copyName); + + try + { + restNodeModel = getRestAPIFactory().getNodeAPI(user, repoTestModel).copy(copyDestinationInfo); + } + catch (Exception e) + { + throw new RuntimeException("Problem copying category.", e); + } + return restNodeModel.getId(); + } + + private Node getNode(String recordId) + { + RepoTestModel repoTestModel = new RepoTestModel() {}; + repoTestModel.setNodeRef(recordId); + return getRestAPIFactory().getNodeAPI(repoTestModel); + } + + private String getTransferId(HttpResponse httpResponse,String nodeRef) { + HttpEntity entity = httpResponse.getEntity(); + String responseString = null; + try { + responseString = EntityUtils.toString(entity, "UTF-8"); + } catch (IOException e) { + throw new RuntimeException(e); + } + JSONObject result = new JSONObject(responseString); + return result + .getJSONObject("results") + .get(nodeRef) + .toString(); + + } + + @Test + @AlfrescoTest(jira = "RM-1622") + public void sameLevelDispositionScheduleStepsPeriodsCalculation() throws Exception { + + // create a category with retention applied on records level + RecordCategory catsameLevel1 = getRestAPIFactory().getFilePlansAPI(rmAdmin) + .createRootRecordCategory(RecordCategory.builder().name(firstCategoryRM3060).build(), + RecordCategory.DEFAULT_FILE_PLAN_ALIAS); + RecordCategory catsameLevel2 = getRestAPIFactory().getFilePlansAPI(rmAdmin) + .createRootRecordCategory(RecordCategory.builder().name(secondCategoryRM3060).build(), + RecordCategory.DEFAULT_FILE_PLAN_ALIAS); + + // create retention schedule applied on records for category 1 + dispositionScheduleService.createCategoryRetentionSchedule(firstCategoryRM3060, true); + + // with retain immediately after record creation date and cut 1 day after record creation date + dispositionScheduleService.addCutOffAfterPeriodStep(firstCategoryRM3060, "day|1", DATE_FILED); + + + // create a folder on the category firstCategoryRM3060 with a complete electronic record + RecordCategoryChild firstFolderRecordCategoryChild = createRecordFolder(catsameLevel1.getId(),firstFolderRM3060); + Record firstElectronicRecord = createElectronicRecord(firstFolderRecordCategoryChild.getId(),electronicRecordRM3060); + + String elRecordFullName = recordsAPI.getRecordFullName(getDataUser().getAdminUser().getUsername(), + getDataUser().getAdminUser().getPassword(),firstFolderRM3060, electronicRecordRM3060); + String elRecordNameNodeRef = recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), elRecordFullName, "/" + firstCategoryRM3060 + "/" + firstFolderRM3060); + + recordsAPI.completeRecord(getDataUser().getAdminUser().getUsername(), + getDataUser().getAdminUser().getPassword(), elRecordFullName); + + // create a folder on the category secondCategoryRM3060 with a non electronic record + RecordCategoryChild secondFolderRecordCategoryChild = createRecordFolder(catsameLevel2.getId(),secondFolderRM3060); + String elRecordNameNodeRefs = recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), elRecordFullName, "/" + firstCategoryRM3060 + "/" + firstFolderRM3060); + + + // link it to the folder in second category through the details page + List recordLists = new ArrayList<>(); + recordLists.add(NODE_REF_WORKSPACE_SPACES_STORE + firstElectronicRecord.getId()); + + linksAPI.linkRecord(getDataUser().getAdminUser().getUsername(), + getDataUser().getAdminUser().getPassword(), HttpStatus.SC_OK,secondCategoryRM3060 + "/" + + secondFolderRM3060, recordLists); + + // edit disposition date + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),editDispositionDateJson(),elRecordNameNodeRefs); + + + } + + @Test (dependsOnMethods = {"sameLevelDispositionScheduleStepsPeriodsCalculation" }) + public void deleteLongestPeriodTestPrecondition() { + // Delete the RM site + getRestAPIFactory().getRMSiteAPI().deleteRMSite(); + + // Verify the status code + assertStatusCode(NO_CONTENT); + } + } diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/FileAsRecordTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/FileAsRecordTests.java new file mode 100644 index 0000000000..cdadcd5ceb --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/FileAsRecordTests.java @@ -0,0 +1,238 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.rm.community.smoke; + +import org.alfresco.dataprep.CMISUtil; +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.rm.community.model.recordfolder.RecordFolderCollection; +import org.alfresco.rest.rm.community.model.user.UserRoles; +import org.alfresco.rest.v0.RecordCategoriesAPI; +import org.alfresco.rest.v0.service.RoleService; +import org.alfresco.test.AlfrescoTest; +import org.alfresco.utility.Utility; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.UserModel; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import java.util.concurrent.atomic.AtomicReference; +import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAlias.UNFILED_RECORDS_CONTAINER_ALIAS; +import static org.alfresco.rest.rm.community.model.user.UserPermissions.PERMISSION_FILING; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.rest.rm.community.utils.CoreUtil.toContentModel; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.springframework.test.util.AssertionErrors.assertTrue; +import static org.testng.Assert.*; + +public class FileAsRecordTests extends BaseRMRestTest { + + private static final String CATEGORY_MANAGER = "catManager" + generateTestPrefix(FileAsRecordTests.class); + private static final String CATEGORY_ADMIN = "catAdmin" + generateTestPrefix(FileAsRecordTests.class); + private static final String FOLDER_MANAGER = "recordFolder" + generateTestPrefix(FileAsRecordTests.class); + private static final String FOLDER_ADMIN = "recordFolder" + generateTestPrefix(FileAsRecordTests.class); + + private UserModel nonRMuser,rmManager; + private SiteModel testSite; + private FileModel document, documentDeclared; + private RecordCategory category_manager, category_admin; + private RecordCategoryChild folder_admin, folder_manager ; + @Autowired + private DataSite dataSite; + @Autowired + private DataContent dataContent; + @Autowired + private RoleService roleService; + @Autowired + private RecordCategoriesAPI recordCategoriesAPI; + /** + * Create preconditions: + *

+     *     1. RM site is created
+     *     2. Two users: user without RM role and a user with RM manager role
+     *     3. Two Record categories with one folder each
+     *     4. User with RM MANAGER role has Filling permission over one category
+     * 
+ */ + @BeforeClass(alwaysRun = true) + public void preconditionForFileAsRecordRecordTests() + { + STEP("Create the RM site if doesn't exist"); + createRMSiteIfNotExists(); + + STEP("Create a user"); + nonRMuser = dataUser.createRandomTestUser("testUser"); + + STEP("Create a collaboration site"); + testSite = dataSite.usingUser(nonRMuser).createPublicRandomSite(); + + STEP("Create a document with the user without RM role"); + document = dataContent.usingSite(testSite) + .usingUser(nonRMuser) + .createContent(CMISUtil.DocumentType.TEXT_PLAIN); + + STEP("Create two categories with two folders"); + category_manager = createRootCategory(CATEGORY_MANAGER); + category_admin = createRootCategory(CATEGORY_ADMIN); + folder_admin = createFolder(category_admin.getId(),FOLDER_ADMIN); + folder_manager = createFolder(category_manager.getId(),FOLDER_MANAGER); + + STEP("Create an rm user and give filling permission over CATEGORY_MANAGER record category"); + RecordCategory recordCategory = new RecordCategory().builder() + .id(category_manager.getId()) + .build(); + rmManager = roleService.createCollaboratorWithRMRoleAndPermission(testSite, recordCategory, + UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING); + } + + /** + * Given I have selected the record folder I want to file my declared record to + * When I confirm the action + * Then the dialog closes + * And the document is now shown as a record in the collaboration site + * And if I navigated to the record folder, as any user who had the right permissions, then I would see the + * record filed + */ + @Test + @AlfrescoTest(jira = "RM-6780") + public void checkFileAsRecordToRecordFolder() throws Exception { + + AtomicReference apiChildren = new AtomicReference<>(); + STEP("Create a document with the user with RM role"); + documentDeclared = dataContent.usingSite(testSite).usingUser(rmManager) + .createContent(new FileModel("checkDeclareAndFileToRecordFolder", FileType.TEXT_PLAIN)); + + STEP("Declare and file into a record folder the document uploaded"); + + getRestAPIFactory().getActionsAPI(rmManager).declareAndFile(documentDeclared, + Utility.buildPath(CATEGORY_MANAGER, FOLDER_MANAGER)); + + STEP("Check the file is a record within the collaboration site"); + + try + { + Utility.sleep(1000, 40000, () -> + { + JSONObject collaboratorSearchJson = getSearchApi().liveSearchForDocuments(rmManager.getUsername(), + rmManager.getPassword(), + documentDeclared.getName()); + assertTrue("Rm Manager not able to find the document.", collaboratorSearchJson.getJSONArray("items").length() != 0); + }); + } + catch (InterruptedException e) + { + fail("InterruptedException received while waiting for results."); + } + + STEP("Check the record is filed into the record folder."); + // Get children from API + // List children from API + try + { + Utility.sleep(1000, 40000, () -> + { + apiChildren.set((RecordFolderCollection) getRestAPIFactory() + .getRecordFolderAPI(rmManager).getRecordFolderChildren(folder_manager.getId(), "include=properties") + .assertThat().entriesListIsNotEmpty().assertThat().entriesListIsNotEmpty()); + }); + } + catch (InterruptedException e) + { + fail("InterruptedException received while waiting for results."); + } + + assertEquals(apiChildren.get() + .getEntries() + .get(0) + .getEntry() + .getProperties() + .getOriginalName(),documentDeclared.getName()); + } + + /** + * Given I have selected the "File As Record" action + * When I confirm the action without selecting a location to file to + * Then the record is declared in the unfiled folder + */ + @Test + @AlfrescoTest (jira = "RM-6780") + public void fileAsRecordToUnfiledRecordFolder() throws Exception { + STEP("Create a document with the user without RM role"); + FileModel inplaceRecord = dataContent.usingSite(testSite).usingUser(rmManager) + .createContent(new FileModel("declareAndFileToIntoUnfiledRecordFolder", + FileType.TEXT_PLAIN)); + + STEP("Click on Declare and file without selecting a record folder"); + getRestAPIFactory().getActionsAPI(rmManager).declareAndFile(inplaceRecord,""); + + STEP("Check the file is declared in unfiled record folder"); + Assert.assertTrue(isMatchingRecordInUnfiledRecords(inplaceRecord), "Record should be filed to Unfiled Records folder"); + } + + @AfterClass(alwaysRun = true) + public void cleanUpForFileAsRecordRecordTests() { + STEP("Delete the collaboration site"); + dataSite.usingUser(nonRMuser).deleteSite(testSite); + + STEP("Empty the trashcan."); + restClient.authenticateUser(nonRMuser).withCoreAPI().usingTrashcan().deleteNodeFromTrashcan(toContentModel(testSite.getId())); + + getRestAPIFactory() + .getUnfiledContainersAPI(rmManager) + .getUnfiledContainerChildren(UNFILED_RECORDS_CONTAINER_ALIAS) + .getEntries() + .stream() + .forEach(x -> getRestAPIFactory() + .getRecordsAPI() + .deleteRecord(x.getEntry().getId())); + + STEP("Cleanup Documents inside folders"); + + STEP("Delete folders"); + getRestAPIFactory().getRecordFolderAPI().deleteRecordFolder(folder_admin.getId()); + getRestAPIFactory().getRecordFolderAPI().deleteRecordFolder(folder_manager.getId()); + + STEP("Delete categories"); + recordCategoriesAPI.deleteCategory(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), category_manager.getName()); + recordCategoriesAPI.deleteCategory(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), category_admin.getName()); + + STEP("Delete Users"); + dataUser.deleteUser(nonRMuser); + dataUser.deleteUser(rmManager); + } +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/FileVersionAsRecordTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/FileVersionAsRecordTests.java new file mode 100644 index 0000000000..09600f9545 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/FileVersionAsRecordTests.java @@ -0,0 +1,138 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.rm.community.smoke; + +import org.alfresco.dataprep.CMISUtil; +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.rm.community.model.recordfolder.RecordFolderCollection; +import org.alfresco.rest.rm.community.model.user.UserRoles; +import org.alfresco.rest.rm.community.records.FileUnfiledRecordsTests; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.rest.v0.RecordCategoriesAPI; +import org.alfresco.rest.v0.RecordsAPI; +import org.alfresco.rest.v0.service.RoleService; +import org.alfresco.test.AlfrescoTest; +import org.alfresco.utility.Utility; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.UserModel; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.concurrent.atomic.AtomicReference; + +import static org.alfresco.rest.rm.community.model.user.UserPermissions.PERMISSION_FILING; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.utility.data.RandomData.getRandomName; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.springframework.test.util.AssertionErrors.assertTrue; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +public class FileVersionAsRecordTests extends BaseRMRestTest { + + private UserModel nonRMuser,rmManager; + private SiteModel testSite; + private FileModel document, documentDeclared; + private RecordCategory category_manager, category_admin; + private RecordCategoryChild folder_admin, folder_manager ; + private static final String CATEGORY_MANAGER = "catManager" + generateTestPrefix(FileAsRecordTests.class); + private static final String CATEGORY_ADMIN = "catAdmin" + generateTestPrefix(FileAsRecordTests.class); + private static final String FOLDER_MANAGER = "recordFolder" + generateTestPrefix(FileAsRecordTests.class); + private static final String FOLDER_ADMIN = "recordFolder" + generateTestPrefix(FileAsRecordTests.class); + + @Autowired + private DataSite dataSite; + @Autowired + private DataContent dataContent; + @Autowired + private RoleService roleService; + + @BeforeClass(alwaysRun = true) + public void preconditionForFileVersionAsRecordTests() + { + STEP("Create the RM site if doesn't exist"); + createRMSiteIfNotExists(); + + STEP("Create a user"); + nonRMuser = dataUser.createRandomTestUser("testUser"); + + STEP("Create a collaboration site"); + testSite = dataSite.usingUser(nonRMuser).createPublicRandomSite(); + + STEP("Create a document with the user without RM role"); + document = dataContent.usingSite(testSite) + .usingUser(nonRMuser) + .createContent(CMISUtil.DocumentType.TEXT_PLAIN); + + STEP("Create two categories with two folders"); + category_manager = createRootCategory(CATEGORY_MANAGER); + category_admin = createRootCategory(CATEGORY_ADMIN); + folder_admin = createFolder(category_admin.getId(),FOLDER_ADMIN); + folder_manager = createFolder(category_manager.getId(),FOLDER_MANAGER); + + STEP("Create an rm user and give filling permission over CATEGORY_MANAGER record category"); + RecordCategory recordCategory = new RecordCategory().builder() + .id(category_manager.getId()) + .build(); + rmManager = roleService.createCollaboratorWithRMRoleAndPermission(testSite, recordCategory, + UserRoles.ROLE_RM_MANAGER, PERMISSION_FILING); + + } + + @Test + @AlfrescoTest (jira = "APPS-1625") + public void fileVersionAsRecordToUnfiledRecordContainer() throws Exception + { + + AtomicReference apiChildren = new AtomicReference<>(); + + STEP("Create a document with the user without RM role"); + FileModel inplaceRecord = dataContent.usingSite(testSite).usingUser(rmManager) + .createContent(new FileModel("declareAndFileToIntoUnfiledRecordFolder", + FileType.TEXT_PLAIN)); + + STEP("Click on Declare and file without selecting a record folder"); + getRestAPIFactory().getActionsAPI(rmManager).declareAndFile(inplaceRecord,""); + + STEP("Check the file is declared in unfiled record folder"); + Assert.assertTrue(isMatchingRecordInUnfiledRecords(inplaceRecord), "Record should be filed to Unfiled Records folder"); + + + } + + +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/FoldersDispositionScheduleTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/FoldersDispositionScheduleTests.java new file mode 100644 index 0000000000..e2a9d7c6da --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/FoldersDispositionScheduleTests.java @@ -0,0 +1,108 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rm.community.smoke; + +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.record.Record; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.v0.RecordFoldersAPI; +import org.alfresco.rest.v0.service.DispositionScheduleService; +import org.alfresco.test.AlfrescoTest; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import static org.alfresco.rest.rm.community.model.recordcategory.RetentionPeriodProperty.CREATED_DATE; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.utility.data.RandomData.getRandomName; +import static org.alfresco.utility.report.log.Step.STEP; + +public class FoldersDispositionScheduleTests extends BaseRMRestTest { + + private RecordCategory Category1; + @Autowired + private DispositionScheduleService dispositionScheduleService; + @Autowired + private RecordFoldersAPI recordFoldersAPI; + private final String TEST_PREFIX = generateTestPrefix(FoldersDispositionScheduleTests.class); + private final String folderDisposition = TEST_PREFIX + "RM-2937 folder ghosting"; + private final String electronicRecord = "RM-2937 electronic 2 record"; + private final String nonElectronicRecord = "RM-2937 non-electronic record"; + + @BeforeClass(alwaysRun = true) + private void setUp(){ + + STEP("Create the RM site if doesn't exist"); + createRMSiteIfNotExists(); + + STEP("Create record category"); + Category1 = createRootCategory(getRandomName("Title")); + } + + @Test + @AlfrescoTest (jira = "RM-2937") + public void foldersDispositionScheduleWithGhosting() { + + //create retention schedule + dispositionScheduleService.createCategoryRetentionSchedule(Category1.getName(), false); + + // add cut off step + dispositionScheduleService.addCutOffAfterPeriodStep(Category1.getName(), "day|2", CREATED_DATE); + + // add destroy step with ghosting + dispositionScheduleService.addDestroyWithGhostingImmediatelyAfterCutOff(Category1.getName()); + + //create folders + RecordCategoryChild FOLDER_DESTROY = createFolder(getAdminUser(),Category1.getId(),folderDisposition); + + Record elRecord = createElectronicRecord(FOLDER_DESTROY.getId(),electronicRecord); + Record nonElRecord = createNonElectronicRecord(FOLDER_DESTROY.getId(),nonElectronicRecord); + + // complete records + completeRecord(elRecord.getId()); + completeRecord(nonElRecord.getId()); + + // edit disposition date + recordFoldersAPI.postFolderAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),editDispositionDateJson(),FOLDER_DESTROY.getName()); + + // cut off the FOLDER_DESTROY + recordFoldersAPI.postFolderAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","cutoff"),FOLDER_DESTROY.getName()); + + // Destroy the FOLDER_DESTROY + recordFoldersAPI.postFolderAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","destroy"),FOLDER_DESTROY.getName()); + } + + @AfterMethod(alwaysRun = true) + private void deletePreconditions() { + deleteRecordCategory(Category1.getId()); + } +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/FoldersDispositionScheduleWithoutGhostRecordTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/FoldersDispositionScheduleWithoutGhostRecordTests.java new file mode 100644 index 0000000000..4bec2fd08c --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/FoldersDispositionScheduleWithoutGhostRecordTests.java @@ -0,0 +1,116 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rm.community.smoke; + +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.record.Record; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.v0.RecordFoldersAPI; +import org.alfresco.rest.v0.service.DispositionScheduleService; +import org.alfresco.test.AlfrescoTest; +import org.alfresco.utility.Utility; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import static org.alfresco.rest.rm.community.model.recordcategory.RetentionPeriodProperty.CREATED_DATE; +import static org.alfresco.rest.rm.community.model.recordcategory.RetentionPeriodProperty.CUT_OFF_DATE; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.utility.data.RandomData.getRandomName; +import static org.alfresco.utility.report.log.Step.STEP; + +public class FoldersDispositionScheduleWithoutGhostRecordTests extends BaseRMRestTest { + + private RecordCategory Category1; + @Autowired + private DispositionScheduleService dispositionScheduleService; + @Autowired + private RecordFoldersAPI recordFoldersAPI; + private final String TEST_PREFIX = generateTestPrefix(FoldersDispositionScheduleWithoutGhostRecordTests.class); + private final String folderDisposition = TEST_PREFIX + "RM-2937 folder ghosting"; + private final String electronicRecord = "RM-2937 electronic 2 record"; + private final String nonElectronicRecord = "RM-2937 non-electronic record"; + + @BeforeClass(alwaysRun = true) + private void setUp(){ + + STEP("Create the RM site if doesn't exist"); + createRMSiteIfNotExists(); + + STEP("Create record category"); + Category1 = createRootCategory(getRandomName("Title")); + } + + @Test + @AlfrescoTest(jira="RM-2937") + public void foldersDispositionScheduleWithoutGhosting() { + + //create retention schedule + dispositionScheduleService.createCategoryRetentionSchedule(Category1.getName(), false); + + // add cut off step + dispositionScheduleService.addCutOffAfterPeriodStep(Category1.getName(), "day|2", CREATED_DATE); + + // add destroy step with ghosting + dispositionScheduleService.addDestroyWithoutGhostingAfterPeriodStep(Category1.getName(), "day|1", CUT_OFF_DATE); + + //create folders + RecordCategoryChild FOLDER_DESTROY = createFolder(getAdminUser(),Category1.getId(),folderDisposition); + + Record elRecord = createElectronicRecord(FOLDER_DESTROY.getId(),electronicRecord); + Record nonElRecord = createNonElectronicRecord(FOLDER_DESTROY.getId(),nonElectronicRecord); + + // complete records + completeRecord(elRecord.getId()); + completeRecord(nonElRecord.getId()); + + // edit disposition date + recordFoldersAPI.postFolderAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),editDispositionDateJson(),FOLDER_DESTROY.getName()); + + // cut off the FOLDER_DESTROY + recordFoldersAPI.postFolderAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","cutoff"),FOLDER_DESTROY.getName()); + + // edit disposition date + recordFoldersAPI.postFolderAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),editDispositionDateJson(),FOLDER_DESTROY.getName()); + + Utility.waitToLoopTime(5,"Waiting for Edit Disposition to be processed"); + + // Destroy the FOLDER_DESTROY + recordFoldersAPI.postFolderAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","destroy"),FOLDER_DESTROY.getName()); + } + + @AfterMethod(alwaysRun = true) + private void deletePreconditions() { + deleteRecordCategory(Category1.getId()); + } +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/RecordRetentionAsOfDateTest.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/RecordRetentionAsOfDateTest.java new file mode 100644 index 0000000000..5427019983 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/RecordRetentionAsOfDateTest.java @@ -0,0 +1,156 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.rm.community.smoke; + +import org.alfresco.rest.core.v0.BaseAPI; +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.record.Record; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.rest.v0.RecordCategoriesAPI; +import org.alfresco.rest.v0.RecordFoldersAPI; +import org.alfresco.rest.v0.RecordsAPI; +import org.alfresco.rest.v0.service.DispositionScheduleService; +import org.alfresco.test.AlfrescoTest; +import org.apache.commons.lang3.time.DateUtils; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.AfterClass; +import org.testng.annotations.Test; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import static org.alfresco.rest.rm.community.base.TestData.DEFAULT_PASSWORD; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.testng.Assert.assertTrue; + +public class RecordRetentionAsOfDateTest extends BaseRMRestTest { + + /** data prep 6services */ + @Autowired + private RMRolesAndActionsAPI rmRolesAndActionsAPI; + @Autowired + private RecordsAPI recordsAPI; + @Autowired + private RecordFoldersAPI recordFoldersAPI; + @Autowired + private RecordCategoriesAPI recordCategoriesAPI; + @Autowired + private DispositionScheduleService dispositionScheduleService; + private RecordCategory Category1; + private final String TEST_PREFIX = generateTestPrefix(RecordRetentionAsOfDateTest.class); + private final String RM_ADMIN = TEST_PREFIX + "rm_admin"; + private final String recordsCategory = TEST_PREFIX + "RM-5733 category"; + private final String folderDisposition = TEST_PREFIX + "RM-5733 folder"; + + private static final String YEAR_MONTH_DAY = "yyyy-MM-dd"; + + @Test + @AlfrescoTest (jira = "RM-5733,RM-5799") + public void checkRetentionAsOfDateForTransferStepWithRetentionAction() { + + // create test precondition + createTestPrecondition(recordsCategory); + + // create disposition schedule + dispositionScheduleService.createCategoryRetentionSchedule(Category1.getName(), true); + + // add cut off step + dispositionScheduleService.addCutOffImmediatelyStep(Category1.getName()); + + // add transfer step + HashMap transferStep = new HashMap<>(); + transferStep.put(BaseAPI.RETENTION_SCHEDULE.RETENTION_PERIOD, "day|1"); + transferStep.put(BaseAPI.RETENTION_SCHEDULE.NAME, "transfer"); + transferStep.put(BaseAPI.RETENTION_SCHEDULE.RETENTION_PERIOD_PROPERTY, "rma:cutOffDate"); + transferStep.put(BaseAPI.RETENTION_SCHEDULE.COMBINE_DISPOSITION_STEP_CONDITIONS, "false"); + transferStep.put(BaseAPI.RETENTION_SCHEDULE.RETENTION_ELIGIBLE_FIRST_EVENT, "true"); + transferStep.put(BaseAPI.RETENTION_SCHEDULE.RETENTION_GHOST, "on"); + transferStep.put(BaseAPI.RETENTION_SCHEDULE.DESCRIPTION, "Transfer after 1 day"); + recordCategoriesAPI.addDispositionScheduleSteps(getAdminUser().getUsername(), + getAdminUser().getPassword(), Category1.getName(), transferStep); + + // create a folder and an electronic and a non-electronic record in it + RecordCategoryChild FOLDER = createFolder(getAdminUser(),Category1.getId(),folderDisposition); + + String nonElectronicRecord = TEST_PREFIX + "RM-5733 non-electronic record"; + Record nonElRecord = createNonElectronicRecord(FOLDER.getId(), nonElectronicRecord); + + // complete records and cut them off + String nonElRecordName = recordsAPI.getRecordFullName(getAdminUser().getUsername(), + getAdminUser().getPassword(), folderDisposition, nonElectronicRecord); + + // complete records and cut them off + completeRecord(nonElRecord.getId()); + + String nonElRecordNameNodeRef = recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), nonElRecordName, "/" + Category1.getName() + "/" + folderDisposition); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","cutoff"),nonElRecordNameNodeRef); + + JSONObject nextDispositionActionJson = recordCategoriesAPI.getNextDispositionAction(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(),nonElRecord.getId()); + + assertTrue(getAsOfDate(nextDispositionActionJson).startsWith(getTomorrow()), + "The retention as of date is not set to tomorrow."); + } + + @AfterClass(alwaysRun = true) + public void cleanUp() { + // delete category + deleteRecordCategory(Category1.getId()); + } + + private void createTestPrecondition(String categoryName) { + createRMSiteIfNotExists(); + + // create "rm admin" user if it does not exist and assign it to RM Administrator role + rmRolesAndActionsAPI.createUserAndAssignToRole( + getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), + RM_ADMIN, DEFAULT_PASSWORD, "Administrator"); + + // create category + STEP("Create category"); + Category1 = createRootCategory(categoryName,"Title"); + } + + private String getAsOfDate(JSONObject nextDispositionActionJson) { + return nextDispositionActionJson.getJSONObject("data").get("asOf").toString(); + } + + private static String getTomorrow() { + Date today = new Date(); + Date tomorrow = DateUtils.addDays(today, 1); + SimpleDateFormat dateFormat = new SimpleDateFormat(YEAR_MONTH_DAY); + return dateFormat.format(tomorrow); + } + +} \ No newline at end of file diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/RecordsDispositionScheduleTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/RecordsDispositionScheduleTests.java new file mode 100644 index 0000000000..90823b3514 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/RecordsDispositionScheduleTests.java @@ -0,0 +1,202 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.rm.community.smoke; + +import org.alfresco.rest.core.v0.RMEvents; +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.record.Record; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.rest.v0.RecordFoldersAPI; +import org.alfresco.rest.v0.RecordsAPI; +import org.alfresco.rest.v0.service.DispositionScheduleService; +import org.alfresco.test.AlfrescoTest; +import org.alfresco.utility.Utility; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.util.EntityUtils; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.Test; +import java.io.IOException; +import java.time.Instant; +import static org.alfresco.rest.rm.community.base.TestData.DEFAULT_PASSWORD; +import static org.alfresco.rest.rm.community.model.recordcategory.RetentionPeriodProperty.CUT_OFF_DATE; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.utility.report.log.Step.STEP; + +/** + * Contains recordsDispositionScheduleWithoutGhosting test which checks disposition schedule cut off, transfer and destroy without maintaining metadata steps applied to records + *

+ * Precondition: + *

+ * RM site created, contains an empty category "RM-2801 disposition for records".

+ * RM user has RM admin role.

+ * A transfer location named "transferred files" is created to which RM user has access + *

+ * Records Disposition Schedule without ghosting + * + * @author Kavit Shah + */ + +public class RecordsDispositionScheduleTests extends BaseRMRestTest { + + /** data prep 6services */ + @Autowired + private RMRolesAndActionsAPI rmRolesAndActionsAPI; + @Autowired + private RecordsAPI recordsAPI; + @Autowired + private RecordFoldersAPI recordFoldersAPI; + @Autowired + private DispositionScheduleService dispositionScheduleService; + private RecordCategory Category1; + private final String TEST_PREFIX = generateTestPrefix(RecordsDispositionScheduleTests.class); + private final String RM_ADMIN = TEST_PREFIX + "rm_admin"; + private final String recordsCategory = TEST_PREFIX + "RM-2801 category"; + private final String folderDisposition = TEST_PREFIX + "RM-2801 folder"; + + @Test + @AlfrescoTest(jira="RM-2801") + public void recordsDispositionScheduleWithoutGhosting() { + + // create test precondition + createTestPrecondition(recordsCategory); + + // create disposition schedule + dispositionScheduleService.createCategoryRetentionSchedule(Category1.getName(), true); + + // add cut off step + dispositionScheduleService.addCutOffImmediatelyStep(Category1.getName()); + + // add transfer step + dispositionScheduleService.addTransferAfterEventStep(Category1.getName(),"transferred records","all_allowances_granted_are_terminated"); + + // add destroy step without retaining metadata + dispositionScheduleService.addDestroyWithoutGhostingAfterPeriodStep(Category1.getName(), "day|1", CUT_OFF_DATE); + + // create a folder and an electronic and a non-electronic record in it + RecordCategoryChild FOLDER_DESTROY = createFolder(getAdminUser(),Category1.getId(),folderDisposition); + + String electronicRecord = "RM-2801 electronic record"; + Record elRecord = createElectronicRecord(FOLDER_DESTROY.getId(), electronicRecord); + String nonElectronicRecord = "RM-2801 non-electronic record"; + Record nonElRecord = createNonElectronicRecord(FOLDER_DESTROY.getId(), nonElectronicRecord); + + // complete records and cut them off + String nonElRecordName = recordsAPI.getRecordFullName(getAdminUser().getUsername(), + getAdminUser().getPassword(), folderDisposition, nonElectronicRecord); + String elRecordName = recordsAPI.getRecordFullName(getAdminUser().getUsername(), + getAdminUser().getPassword(), folderDisposition, electronicRecord); + + // complete records and cut them off + completeRecord(elRecord.getId()); + completeRecord(nonElRecord.getId()); + + String nonElRecordNameNodeRef = recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), nonElRecordName, "/" + Category1.getName() + "/" + folderDisposition); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","cutoff"),nonElRecordNameNodeRef); + + String elRecordNameNodeRef = recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), elRecordName, "/" + Category1.getName() + "/" + folderDisposition); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","cutoff"),elRecordNameNodeRef); + + // ensure the complete event action is displayed for both events + rmRolesAndActionsAPI.completeEvent(getAdminUser().getUsername(), + getAdminUser().getPassword(), nonElRecordName, RMEvents.ALL_ALLOWANCES_GRANTED_ARE_TERMINATED, Instant.now()); + rmRolesAndActionsAPI.completeEvent(getAdminUser().getUsername(), + getAdminUser().getPassword(), elRecordName, RMEvents.ALL_ALLOWANCES_GRANTED_ARE_TERMINATED, Instant.now()); + + // Create and Complete transfer + HttpResponse nonElRecordNameHttpResponse = recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","transfer"),recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), nonElRecordName, "/" + Category1.getName() + "/" + folderDisposition)); + + String nonElRecordNameTransferId = getTransferId(nonElRecordNameHttpResponse,nonElRecordNameNodeRef); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","transferComplete"),nonElRecordNameTransferId); + + HttpResponse elRecordNameHttpResponse = recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","transfer"),recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), elRecordName, "/" + Category1.getName() + "/" + folderDisposition)); + + String elRecordNameTransferId = getTransferId(elRecordNameHttpResponse,elRecordNameNodeRef); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","transferComplete"),elRecordNameTransferId); + + // edit the disposition schedule date to current date + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),editDispositionDateJson(),nonElRecordNameNodeRef); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),editDispositionDateJson(),elRecordNameNodeRef); + + Utility.waitToLoopTime(5,"Waiting for Edit Disposition to be processed"); + + // destroy records + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","destroy"),nonElRecordNameNodeRef); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","destroy"),elRecordNameNodeRef); + + // delete category + deleteRecordCategory(Category1.getId()); + } + + private void createTestPrecondition(String categoryName) { + createRMSiteIfNotExists(); + + // create "rm admin" user if it does not exist and assign it to RM Administrator role + rmRolesAndActionsAPI.createUserAndAssignToRole( + getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), + RM_ADMIN, DEFAULT_PASSWORD, "Administrator"); + + // create category + STEP("Create two category"); + Category1 = createRootCategory(categoryName,"Title"); + } + + private String getTransferId(HttpResponse httpResponse,String nodeRef) { + HttpEntity entity = httpResponse.getEntity(); + String responseString = null; + try { + responseString = EntityUtils.toString(entity, "UTF-8"); + } catch (IOException e) { + throw new RuntimeException(e); + } + JSONObject result = new JSONObject(responseString); + return result + .getJSONObject("results") + .get(nodeRef) + .toString(); + + } +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/RecordsDispositionScheduleWithGhostingTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/RecordsDispositionScheduleWithGhostingTests.java new file mode 100644 index 0000000000..07742a0767 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/RecordsDispositionScheduleWithGhostingTests.java @@ -0,0 +1,200 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.rm.community.smoke; + +import org.alfresco.rest.core.v0.RMEvents; +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.record.Record; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.rest.v0.RecordFoldersAPI; +import org.alfresco.rest.v0.RecordsAPI; +import org.alfresco.rest.v0.service.DispositionScheduleService; +import org.alfresco.test.AlfrescoTest; +import org.alfresco.utility.Utility; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.util.EntityUtils; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.Test; +import java.io.IOException; +import java.time.Instant; +import static org.alfresco.rest.rm.community.base.TestData.DEFAULT_PASSWORD; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.utility.report.log.Step.STEP; + +/** + * Contains recordsDispositionScheduleWithGhosting test which checks disposition schedule cut off, transfer and destroy with maintaining record metadata steps applied to records + *

+ * Precondition: + *

+ * RM site created, contains an empty category "RM-2937 disposition for records with ghosting".

+ * RM user has RM admin role.

+ * A transfer location named "transferred files with ghosting" is created to which RM user has access + *

+ * Records Disposition Schedule with ghosting + * + * @author Kavit Shah + */ +public class RecordsDispositionScheduleWithGhostingTests extends BaseRMRestTest { + + /** data prep 6services */ + @Autowired + private RMRolesAndActionsAPI rmRolesAndActionsAPI; + @Autowired + private RecordsAPI recordsAPI; + @Autowired + private RecordFoldersAPI recordFoldersAPI; + @Autowired + private DispositionScheduleService dispositionScheduleService; + private RecordCategory Category1; + private final String TEST_PREFIX = generateTestPrefix(RecordsDispositionScheduleTests.class); + private final String RM_ADMIN = TEST_PREFIX + "rm_admin"; + private final String recordsCategory = TEST_PREFIX + "RM-2801 category"; + private final String folderDisposition = TEST_PREFIX + "RM-2801 folder"; + + @Test + @AlfrescoTest(jira="RM-2801") + public void recordsDispositionScheduleWithGhosting() { + + // create test precondition + createTestPrecondition(recordsCategory); + + // create disposition schedule + dispositionScheduleService.createCategoryRetentionSchedule(Category1.getName(), true); + + // add cut off step + dispositionScheduleService.addCutOffImmediatelyStep(Category1.getName()); + + // add transfer step + dispositionScheduleService.addTransferAfterEventStep(Category1.getName(),"transferred records","all_allowances_granted_are_terminated"); + + // add destroy step without retaining metadata + dispositionScheduleService.addDestroyWithGhostingImmediatelyAfterCutOff(Category1.getName()); + + // create a folder and an electronic and a non-electronic record in it + RecordCategoryChild FOLDER_DESTROY = createFolder(getAdminUser(),Category1.getId(),folderDisposition); + + String electronicRecord = "RM-2801 electronic record"; + Record elRecord = createElectronicRecord(FOLDER_DESTROY.getId(), electronicRecord); + String nonElectronicRecord = "RM-2801 non-electronic record"; + Record nonElRecord = createNonElectronicRecord(FOLDER_DESTROY.getId(), nonElectronicRecord); + + // complete records and cut them off + String nonElRecordName = recordsAPI.getRecordFullName(getAdminUser().getUsername(), + getAdminUser().getPassword(), folderDisposition, nonElectronicRecord); + String elRecordName = recordsAPI.getRecordFullName(getAdminUser().getUsername(), + getAdminUser().getPassword(), folderDisposition, electronicRecord); + + // complete records and cut them off + completeRecord(elRecord.getId()); + completeRecord(nonElRecord.getId()); + + String nonElRecordNameNodeRef = recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), nonElRecordName, "/" + Category1.getName() + "/" + folderDisposition); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","cutoff"),nonElRecordNameNodeRef); + + String elRecordNameNodeRef = recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), elRecordName, "/" + Category1.getName() + "/" + folderDisposition); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","cutoff"),elRecordNameNodeRef); + + // ensure the complete event action is displayed for both events + rmRolesAndActionsAPI.completeEvent(getAdminUser().getUsername(), + getAdminUser().getPassword(), nonElRecordName, RMEvents.ALL_ALLOWANCES_GRANTED_ARE_TERMINATED, Instant.now()); + rmRolesAndActionsAPI.completeEvent(getAdminUser().getUsername(), + getAdminUser().getPassword(), elRecordName, RMEvents.ALL_ALLOWANCES_GRANTED_ARE_TERMINATED, Instant.now()); + + // Create and Complete transfer + HttpResponse nonElRecordNameHttpResponse = recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","transfer"),recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), nonElRecordName, "/" + Category1.getName() + "/" + folderDisposition)); + + String nonElRecordNameTransferId = getTransferId(nonElRecordNameHttpResponse,nonElRecordNameNodeRef); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","transferComplete"),nonElRecordNameTransferId); + + HttpResponse elRecordNameHttpResponse = recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","transfer"),recordsAPI.getRecordNodeRef(getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), elRecordName, "/" + Category1.getName() + "/" + folderDisposition)); + + String elRecordNameTransferId = getTransferId(elRecordNameHttpResponse,elRecordNameNodeRef); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","transferComplete"),elRecordNameTransferId); + + // edit the disposition schedule date to current date + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),editDispositionDateJson(),nonElRecordNameNodeRef); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),editDispositionDateJson(),elRecordNameNodeRef); + + Utility.waitToLoopTime(5,"Waiting for Edit Disposition to be processed"); + + // destroy records + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","destroy"),nonElRecordNameNodeRef); + recordFoldersAPI.postRecordAction(getAdminUser().getUsername(), + getAdminUser().getPassword(),new JSONObject().put("name","destroy"),elRecordNameNodeRef); + + // delete category + deleteRecordCategory(Category1.getId()); + } + + private void createTestPrecondition(String categoryName) { + createRMSiteIfNotExists(); + + // create "rm admin" user if it does not exist and assign it to RM Administrator role + rmRolesAndActionsAPI.createUserAndAssignToRole( + getDataUser().usingAdmin().getAdminUser().getUsername(), + getDataUser().usingAdmin().getAdminUser().getPassword(), + RM_ADMIN, DEFAULT_PASSWORD, "Administrator"); + + // create category + STEP("Create two category"); + Category1 = createRootCategory(categoryName,"Title"); + } + + private String getTransferId(HttpResponse httpResponse,String nodeRef) { + HttpEntity entity = httpResponse.getEntity(); + String responseString = null; + try { + responseString = EntityUtils.toString(entity, "UTF-8"); + } catch (IOException e) { + throw new RuntimeException(e); + } + JSONObject result = new JSONObject(responseString); + return result + .getJSONObject("results") + .get(nodeRef) + .toString(); + + } +} diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/UnfiledRecordsRuleTests.java b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/UnfiledRecordsRuleTests.java new file mode 100644 index 0000000000..a7023a1723 --- /dev/null +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/java/org/alfresco/rest/rm/community/smoke/UnfiledRecordsRuleTests.java @@ -0,0 +1,124 @@ +/* + * #%L + * Alfresco Records Management Module + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rm.community.smoke; + + +import org.alfresco.rest.rm.community.base.BaseRMRestTest; +import org.alfresco.rest.rm.community.model.record.RecordContent; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategory; +import org.alfresco.rest.rm.community.model.recordcategory.RecordCategoryChild; +import org.alfresco.rest.rm.community.model.rules.ActionsOnRule; +import org.alfresco.rest.rm.community.model.rules.RuleDefinition; +import org.alfresco.rest.rm.community.model.unfiledcontainer.UnfiledContainer; +import org.alfresco.rest.rm.community.model.unfiledcontainer.UnfiledContainerChild; +import org.alfresco.rest.rm.community.model.unfiledcontainer.UnfiledContainerChildProperties; +import org.alfresco.rest.v0.RMRolesAndActionsAPI; +import org.alfresco.rest.v0.RulesAPI; +import org.alfresco.test.AlfrescoTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import static org.alfresco.rest.core.v0.BaseAPI.NODE_PREFIX; +import static org.alfresco.rest.rm.community.base.TestData.ELECTRONIC_RECORD_NAME; +import static org.alfresco.rest.rm.community.base.TestData.NONELECTRONIC_RECORD_NAME; +import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentAlias.UNFILED_RECORDS_CONTAINER_ALIAS; +import static org.alfresco.rest.rm.community.model.fileplancomponents.FilePlanComponentType.*; +import static org.alfresco.rest.rm.community.util.CommonTestUtils.generateTestPrefix; +import static org.alfresco.utility.data.RandomData.getRandomName; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.springframework.http.HttpStatus.*; +public class UnfiledRecordsRuleTests extends BaseRMRestTest { + + @Autowired + private RMRolesAndActionsAPI rmRolesAndActionsAPI; + private final String TEST_PREFIX = generateTestPrefix(CreateCategoriesTests.class); + private final String RM_ADMIN = TEST_PREFIX + "rm_admin"; + private RecordCategory Category2; + private RecordCategoryChild Folder2; + @Autowired + private RulesAPI rulesAPI; + + @Test + @AlfrescoTest(jira = "RM-2794") + public void unfiledRecordsRule() { + + + STEP("Create the RM site if doesn't exist"); + createRMSiteIfNotExists(); + + STEP("Create RM Admin user"); + rmRolesAndActionsAPI.createUserAndAssignToRole(getAdminUser().getUsername(), getAdminUser().getPassword(), RM_ADMIN, + getAdminUser().getPassword(), + "Administrator"); + + STEP("Create record categories and record folders"); + Category2 = createRootCategory(getRandomName("recordCategory")); + Folder2 = createFolder(Category2.getId(), getRandomName("recordFolder")); + + STEP("Get the unfiled records container"); + UnfiledContainer container = getRestAPIFactory().getUnfiledContainersAPI().getUnfiledContainer(UNFILED_RECORDS_CONTAINER_ALIAS); + + // Check the response code + assertStatusCode(OK); + + //create a rule + RuleDefinition ruleDefinition = RuleDefinition.createNewRule().title("name").description("description") + .applyToChildren(true) + .actions(Collections.singletonList(ActionsOnRule.FILE_TO.getActionValue())); + rulesAPI.createRule(getAdminUser().getUsername(), getAdminUser().getPassword(), NODE_PREFIX + container.getId(), ruleDefinition); + + //upload an electronic record + UnfiledContainerChild electronicRecord = UnfiledContainerChild.builder() + .name(ELECTRONIC_RECORD_NAME) + .nodeType(CONTENT_TYPE) + .content(RecordContent.builder().mimeType("text/plain").build()) + .build(); + assertStatusCode(OK); + + + // create a non-electronic record + UnfiledContainerChild nonelectronicRecord = UnfiledContainerChild.builder() + .properties(UnfiledContainerChildProperties.builder() + .description(NONELECTRONIC_RECORD_NAME) + .title("Title") + .build()) + .name(NONELECTRONIC_RECORD_NAME) + .nodeType(NON_ELECTRONIC_RECORD_TYPE) + .build(); + assertStatusCode(OK); + + //delete the record created, delete the rule from UnfilledRecord page, delete the category created + rulesAPI.deleteAllRulesOnContainer(getAdminUser().getUsername(), getAdminUser().getPassword(), NODE_PREFIX + container.getId()); + deleteRecordCategory(Category2.getId()); + assertStatusCode(NO_CONTENT); + + } + + } diff --git a/amps/ags/rm-community/pom.xml b/amps/ags/rm-community/pom.xml index 4f8ff01d1d..25287472cf 100644 --- a/amps/ags/rm-community/pom.xml +++ b/amps/ags/rm-community/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-governance-services-community-parent - 17.43-SNAPSHOT + 20.5-SNAPSHOT diff --git a/amps/ags/rm-community/rm-community-repo/.env b/amps/ags/rm-community/rm-community-repo/.env index ca3c68b5dc..c6c00a9d5d 100644 --- a/amps/ags/rm-community/rm-community-repo/.env +++ b/amps/ags/rm-community/rm-community-repo/.env @@ -1,3 +1,3 @@ -SOLR6_TAG=2.0.3 -POSTGRES_TAG=13.3 -ACTIVEMQ_TAG=5.16.1 +SOLR6_TAG=2.0.5 +POSTGRES_TAG=14.4 +ACTIVEMQ_TAG=5.17.1-jre11-rockylinux8 diff --git a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/alfresco-global.properties b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/alfresco-global.properties index a669cf8351..e6f4de0337 100644 --- a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/alfresco-global.properties +++ b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/alfresco-global.properties @@ -109,6 +109,10 @@ rm.completerecord.mandatorypropertiescheck.enabled=true # rm.patch.v22.convertToStandardFilePlan=false +# +# Max Batch size for adding the associations between the frozen nodes and the hold +rm.patch.v35.holdNewChildAssocPatch.batchSize=1000 + # Permission mapping # these take a comma separated string of permissions from org.alfresco.service.cmr.security.PermissionService # read maps to ReadRecords and write to FileRecords diff --git a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/patch/rm-patch-v35-context.xml b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/patch/rm-patch-v35-context.xml index 6159adab5f..c29851a583 100644 --- a/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/patch/rm-patch-v35-context.xml +++ b/amps/ags/rm-community/rm-community-repo/config/alfresco/module/org_alfresco_module_rm/patch/rm-patch-v35-context.xml @@ -17,5 +17,6 @@ + diff --git a/amps/ags/rm-community/rm-community-repo/pom.xml b/amps/ags/rm-community/rm-community-repo/pom.xml index 3e4f814212..477ddf77eb 100644 --- a/amps/ags/rm-community/rm-community-repo/pom.xml +++ b/amps/ags/rm-community/rm-community-repo/pom.xml @@ -8,7 +8,7 @@ org.alfresco alfresco-governance-services-community-repo-parent - 17.43-SNAPSHOT + 20.5-SNAPSHOT @@ -434,7 +434,7 @@ - alfresco/alfresco-activemq:${dependency.activemq.version} + alfresco/alfresco-activemq:${dependency.activemq.version}-jre11-rockylinux8 ${activemq.port1}:${activemq.port1} @@ -505,7 +505,7 @@ - alfresco/alfresco-activemq:${dependency.activemq.version} + alfresco/alfresco-activemq:${dependency.activemq.version}-jre11-rockylinux8 ${activemq.port1}:${activemq.port1} diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java index 04def910c1..1603e32c9f 100644 --- a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/jscript/app/JSONConversionComponent.java @@ -38,9 +38,12 @@ import java.util.List; import java.util.Map; import java.util.Set; +import lombok.extern.slf4j.Slf4j; import org.alfresco.model.ContentModel; import org.alfresco.module.org_alfresco_module_rm.capability.CapabilityService; import org.alfresco.module.org_alfresco_module_rm.capability.impl.ViewRecordsCapability; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionAction; +import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionActionDefinition; import org.alfresco.module.org_alfresco_module_rm.disposition.DispositionService; import org.alfresco.module.org_alfresco_module_rm.event.EventCompletionDetails; import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanComponentKind; @@ -76,6 +79,7 @@ import org.json.simple.JSONObject; * * @author Roy Wetherall */ +@Slf4j public class JSONConversionComponent extends org.alfresco.repo.jscript.app.JSONConversionComponent implements NodeServicePolicies.OnDeleteNodePolicy, NodeServicePolicies.OnCreateNodePolicy @@ -515,17 +519,25 @@ public class JSONConversionComponent extends org.alfresco.repo.jscript.app.JS AuthenticationUtil.runAsSystem((RunAsWork) () -> { //Add details of the next incomplete event in the disposition schedule - if (dispositionService.getNextDispositionAction(nodeRef) != null) + DispositionAction nextDispositionAction = dispositionService.getNextDispositionAction(nodeRef); + if (nextDispositionAction != null) { - for (EventCompletionDetails details : dispositionService.getNextDispositionAction(nodeRef).getEventCompletionDetails()) + for (EventCompletionDetails details : nextDispositionAction.getEventCompletionDetails()) { if (!details.isEventComplete()) { + DispositionActionDefinition dispositionActionDefinition = nextDispositionAction.getDispositionActionDefinition(); HashMap properties = (HashMap) rmNodeValues.get("properties"); - properties.put("combineDispositionStepConditions", nodeService.getProperty(dispositionService.getNextDispositionAction(nodeRef).getDispositionActionDefinition().getNodeRef(), PROP_COMBINE_DISPOSITION_STEP_CONDITIONS)); properties.put("incompleteDispositionEvent", details.getEventName()); - properties.put("dispositionEventCombination", nodeService.getProperty(dispositionService.getNextDispositionAction(nodeRef).getDispositionActionDefinition().getNodeRef(), PROP_DISPOSITION_EVENT_COMBINATION)); - + if(dispositionActionDefinition == null) + { + log.debug("Disposition action definition for disposition action "+ nextDispositionAction.getName() +" has been removed or never exist"); + } + else + { + properties.put("combineDispositionStepConditions", nodeService.getProperty(dispositionActionDefinition.getNodeRef(), PROP_COMBINE_DISPOSITION_STEP_CONDITIONS)); + properties.put("dispositionEventCombination", nodeService.getProperty(dispositionActionDefinition.getNodeRef(), PROP_DISPOSITION_EVENT_COMBINATION)); + } break; } } diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/patch/v35/RMv35HoldNewChildAssocPatch.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/patch/v35/RMv35HoldNewChildAssocPatch.java index 12f2fc30a0..507f61cdc1 100644 --- a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/patch/v35/RMv35HoldNewChildAssocPatch.java +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/module/org_alfresco_module_rm/patch/v35/RMv35HoldNewChildAssocPatch.java @@ -30,6 +30,9 @@ import static org.alfresco.model.ContentModel.ASSOC_CONTAINS; import static org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementCustomModel.RM_CUSTOM_URI; import static org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel.ASSOC_FROZEN_CONTENT; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; import java.util.List; import org.alfresco.model.ContentModel; @@ -37,11 +40,14 @@ import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanService; import org.alfresco.module.org_alfresco_module_rm.hold.HoldService; import org.alfresco.module.org_alfresco_module_rm.patch.AbstractModulePatch; import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.RegexQNamePattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Patch to create new hold child association to link the record to the hold @@ -52,8 +58,15 @@ import org.alfresco.service.namespace.RegexQNamePattern; */ public class RMv35HoldNewChildAssocPatch extends AbstractModulePatch { + /** logger */ + protected static final Logger LOGGER = LoggerFactory.getLogger(RMv35HoldNewChildAssocPatch.class); + /** A name for the associations created by this patch. */ - protected static final QName PATCH_ASSOC_NAME = QName.createQName(RM_CUSTOM_URI, RMv35HoldNewChildAssocPatch.class.getSimpleName()); + protected static final QName PATCH_ASSOC_NAME = QName.createQName(RM_CUSTOM_URI, + RMv35HoldNewChildAssocPatch.class.getSimpleName()); + + /** The batch size for processing frozen nodes. */ + private int batchSize = 1000; /** * File plan service interface @@ -75,7 +88,8 @@ public class RMv35HoldNewChildAssocPatch extends AbstractModulePatch /** * Setter for fileplanservice * - * @param filePlanService File plan service interface + * @param filePlanService + * File plan service interface */ public void setFilePlanService(FilePlanService filePlanService) { @@ -85,7 +99,8 @@ public class RMv35HoldNewChildAssocPatch extends AbstractModulePatch /** * Setter for hold service * - * @param holdService Hold service interface. + * @param holdService + * Hold service interface. */ public void setHoldService(HoldService holdService) { @@ -95,7 +110,8 @@ public class RMv35HoldNewChildAssocPatch extends AbstractModulePatch /** * Setter for node service * - * @param nodeService Interface for public and internal node and store operations. + * @param nodeService + * Interface for public and internal node and store operations. */ public void setNodeService(NodeService nodeService) { @@ -112,33 +128,49 @@ public class RMv35HoldNewChildAssocPatch extends AbstractModulePatch this.behaviourFilter = behaviourFilter; } + /** + * Setter for maximum batch size + * + * @param maxBatchSize + * The max amount of associations to be created between the frozen nodes and the hold in a transaction + */ + public void setBatchSize(int batchSize) + { + this.batchSize = batchSize; + } + @Override public void applyInternal() { behaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE); behaviourFilter.disableBehaviour(ContentModel.ASPECT_VERSIONABLE); + try { + int patchedNodesCounter = 0; + for (NodeRef filePlan : filePlanService.getFilePlans()) { for (NodeRef hold : holdService.getHolds(filePlan)) { - List frozenAssoc = nodeService.getChildAssocs(hold, ASSOC_FROZEN_CONTENT, RegexQNamePattern.MATCH_ALL); - for (ChildAssociationRef ref : frozenAssoc) + LOGGER.debug("Analyzing hold {}", hold.getId()); + + BatchWorker batchWorker = new BatchWorker(hold); + + LOGGER.debug("Hold has {} items to be analyzed", batchWorker.getWorkSize()); + + while (batchWorker.hasMoreResults()) { - NodeRef childNodeRef = ref.getChildRef(); - // In testing we found that this was returning more than just "contains" associations. - // Possibly this is due to the code in Node2ServiceImpl.getParentAssocs not using the second parameter. - List parentAssocs = nodeService.getParentAssocs(childNodeRef, ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); - boolean childContainedByHold = - parentAssocs.stream().anyMatch(entry -> entry.getParentRef().equals(hold) && entry.getTypeQName().equals(ASSOC_CONTAINS)); - if (!childContainedByHold) - { - nodeService.addChild(hold, childNodeRef, ASSOC_CONTAINS, PATCH_ASSOC_NAME); - } + processBatch(hold, batchWorker); } + + LOGGER.debug("Patched {} items in hold", batchWorker.getTotalPatchedNodes()); + + patchedNodesCounter += batchWorker.getTotalPatchedNodes(); } } + + LOGGER.debug("Patch applied to {} children across all holds", patchedNodesCounter); } finally { @@ -146,4 +178,92 @@ public class RMv35HoldNewChildAssocPatch extends AbstractModulePatch behaviourFilter.enableBehaviour(ContentModel.ASPECT_VERSIONABLE); } } + + private void processBatch(NodeRef hold, BatchWorker batch) + { + transactionService.getRetryingTransactionHelper().doInTransaction(() -> { + + Collection childRefs = batch.getNextWork(); + + LOGGER.debug("Processing batch of {} children in hold", childRefs.size()); + + for (ChildAssociationRef child : childRefs) + { + NodeRef childNodeRef = child.getChildRef(); + + if (!isChildContainedByHold(hold, childNodeRef)) + { + nodeService.addChild(hold, childNodeRef, ASSOC_CONTAINS, PATCH_ASSOC_NAME); + batch.countPatchedNode(); + } + } + + return null; + }, false, true); + } + + private boolean isChildContainedByHold(NodeRef hold, NodeRef child) + { + // In testing we found that this was returning more than just "contains" associations. + // Possibly this is due to the code in Node2ServiceImpl.getParentAssocs not using the second + // parameter. + List parentAssocs = nodeService.getParentAssocs(child, ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL); + return parentAssocs.stream() + .anyMatch(entry -> entry.getParentRef().equals(hold) && entry.getTypeQName().equals(ASSOC_CONTAINS)); + } + + private class BatchWorker + { + NodeRef hold; + int totalPatchedNodes = 0; + int workSize; + Iterator iterator; + + public BatchWorker(NodeRef hold) + { + this.hold = hold; + setupHold(); + } + + public boolean hasMoreResults() + { + return iterator == null ? true : iterator.hasNext(); + } + + public void countPatchedNode() + { + this.totalPatchedNodes += 1; + } + + public int getTotalPatchedNodes() + { + return totalPatchedNodes; + } + + public int getWorkSize() + { + return workSize; + } + + public void setupHold() + { + // Get child assocs without preloading + List holdChildren = nodeService.getChildAssocs(hold, ASSOC_FROZEN_CONTENT, + RegexQNamePattern.MATCH_ALL, Integer.MAX_VALUE, false); + this.iterator = holdChildren.listIterator(); + this.workSize = holdChildren.size(); + } + + public Collection getNextWork() + { + List frozenNodes = new ArrayList(batchSize); + while (iterator.hasNext() && frozenNodes.size() < batchSize) + { + frozenNodes.add(iterator.next()); + } + return frozenNodes; + } + + } + } diff --git a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/repo/rule/ExtendedRuleServiceImpl.java b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/repo/rule/ExtendedRuleServiceImpl.java index 1c2738abcb..0cb33b926c 100644 --- a/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/repo/rule/ExtendedRuleServiceImpl.java +++ b/amps/ags/rm-community/rm-community-repo/source/java/org/alfresco/repo/rule/ExtendedRuleServiceImpl.java @@ -156,25 +156,24 @@ public class ExtendedRuleServiceImpl extends RuleServiceImpl * @see org.alfresco.repo.rule.RuleServiceImpl#saveRule(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.rule.Rule) */ @Override - public void saveRule(final NodeRef nodeRef, final Rule rule) + public Rule saveRule(final NodeRef nodeRef, final Rule rule) { validateWormLockRuleAction(rule); if (filePlanService.isFilePlanComponent(nodeRef)) { - AuthenticationUtil.runAsSystem(new RunAsWork() + return AuthenticationUtil.runAsSystem(new RunAsWork() { @Override - public Void doWork() + public Rule doWork() { - ExtendedRuleServiceImpl.super.saveRule(nodeRef, rule); - return null; + return ExtendedRuleServiceImpl.super.saveRule(nodeRef, rule); } }); } else { - super.saveRule(nodeRef, rule); + return super.saveRule(nodeRef, rule); } } diff --git a/amps/ags/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/rule/FilePlanRuleInheritanceTest.java b/amps/ags/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/rule/FilePlanRuleInheritanceTest.java index 4a09476861..5a0903be74 100644 --- a/amps/ags/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/rule/FilePlanRuleInheritanceTest.java +++ b/amps/ags/rm-community/rm-community-repo/test/java/org/alfresco/module/org_alfresco_module_rm/test/integration/rule/FilePlanRuleInheritanceTest.java @@ -82,13 +82,7 @@ public class FilePlanRuleInheritanceTest extends BaseRMTestCase public void given() { filePlan = createFilePlan(); - - // create a rule that applies to childre - Action completeRecordAction = actionService.createAction(DeclareRecordAction.NAME); - rule = new Rule(); - rule.setRuleType("inbound"); - rule.setAction(completeRecordAction); - rule.applyToChildren(true); + rule = createRuleThatAppliesToChildren(); } public void when() @@ -127,13 +121,7 @@ public class FilePlanRuleInheritanceTest extends BaseRMTestCase public void given() { filePlan = createFilePlan(); - - // create a rule that applies to childre - Action completeRecordAction = actionService.createAction(DeclareRecordAction.NAME); - rule = new Rule(); - rule.setRuleType("inbound"); - rule.setAction(completeRecordAction); - rule.applyToChildren(true); + rule = createRuleThatAppliesToChildren(); } public void when() @@ -171,13 +159,7 @@ public class FilePlanRuleInheritanceTest extends BaseRMTestCase public void given() { filePlan = createFilePlan(); - - // create a rule that applies to childre - Action completeRecordAction = actionService.createAction(DeclareRecordAction.NAME); - rule = new Rule(); - rule.setRuleType("inbound"); - rule.setAction(completeRecordAction); - rule.applyToChildren(true); + rule = createRuleThatAppliesToChildren(); } public void when() @@ -215,13 +197,7 @@ public class FilePlanRuleInheritanceTest extends BaseRMTestCase public void given() { filePlan = createFilePlan(); - - // create a rule that applies to childre - Action completeRecordAction = actionService.createAction(DeclareRecordAction.NAME); - rule = new Rule(); - rule.setRuleType("inbound"); - rule.setAction(completeRecordAction); - rule.applyToChildren(true); + rule = createRuleThatAppliesToChildren(); } public void when() @@ -261,13 +237,7 @@ public class FilePlanRuleInheritanceTest extends BaseRMTestCase { filePlan = createFilePlan(); recordCategory = filePlanService.createRecordCategory(filePlan, GUID.generate()); - - // create a rule that applies to childre - Action completeRecordAction = actionService.createAction(DeclareRecordAction.NAME); - rule = new Rule(); - rule.setRuleType("inbound"); - rule.setAction(completeRecordAction); - rule.applyToChildren(true); + rule = createRuleThatAppliesToChildren(); } public void when() @@ -286,4 +256,15 @@ public class FilePlanRuleInheritanceTest extends BaseRMTestCase } }); } + + private Rule createRuleThatAppliesToChildren() + { + Action completeRecordAction = actionService.createAction(DeclareRecordAction.NAME); + Rule rule = new Rule(); + rule.setRuleType("inbound"); + rule.setTitle("Rule name"); + rule.setAction(completeRecordAction); + rule.applyToChildren(true); + return rule; + } } diff --git a/amps/ags/rm-community/rm-community-repo/test/resources/alfresco/version.properties b/amps/ags/rm-community/rm-community-repo/test/resources/alfresco/version.properties index 63b9b4bb6d..ca959b89a7 100644 --- a/amps/ags/rm-community/rm-community-repo/test/resources/alfresco/version.properties +++ b/amps/ags/rm-community/rm-community-repo/test/resources/alfresco/version.properties @@ -4,7 +4,7 @@ # Version label version.major=7 -version.minor=3 +version.minor=4 version.revision=0 version.label= diff --git a/amps/ags/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/patch/v35/RMv35HoldNewChildAssocPatchUnitTest.java b/amps/ags/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/patch/v35/RMv35HoldNewChildAssocPatchUnitTest.java index e799923b1c..4fabc47300 100644 --- a/amps/ags/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/patch/v35/RMv35HoldNewChildAssocPatchUnitTest.java +++ b/amps/ags/rm-community/rm-community-repo/unit-test/java/org/alfresco/module/org_alfresco_module_rm/patch/v35/RMv35HoldNewChildAssocPatchUnitTest.java @@ -34,7 +34,9 @@ import static org.alfresco.model.ContentModel.ASSOC_CONTAINS; import static org.alfresco.module.org_alfresco_module_rm.model.RecordsManagementModel.ASSOC_FROZEN_CONTENT; import static org.alfresco.module.org_alfresco_module_rm.patch.v35.RMv35HoldNewChildAssocPatch.PATCH_ASSOC_NAME; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -51,16 +53,21 @@ import java.util.Set; import org.alfresco.module.org_alfresco_module_rm.fileplan.FilePlanService; import org.alfresco.module.org_alfresco_module_rm.hold.HoldService; import org.alfresco.repo.policy.BehaviourFilter; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.service.transaction.TransactionService; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; /** * RM V3.5 Create new hold child association to link the record to the hold @@ -81,6 +88,12 @@ public class RMv35HoldNewChildAssocPatchUnitTest @Mock private BehaviourFilter mockBehaviourFilter; + @Mock + private TransactionService mockTransactionService; + + @Mock + private RetryingTransactionHelper mockRetryingTransactionHelper; + @InjectMocks private RMv35HoldNewChildAssocPatch patch; @@ -112,25 +125,63 @@ public class RMv35HoldNewChildAssocPatchUnitTest /** * Test secondary associations are created for held items so that they are "contained" in the hold. */ + @SuppressWarnings("unchecked") @Test public void testAddChildDuringUpgrade() { when(mockFilePlanService.getFilePlans()).thenReturn(fileplans); when(mockHoldService.getHolds(filePlanRef)).thenReturn(holds); - when(mockNodeService.getChildAssocs(holdRef, ASSOC_FROZEN_CONTENT, RegexQNamePattern.MATCH_ALL)).thenReturn(childAssocs); + when(mockNodeService.getChildAssocs(holdRef, ASSOC_FROZEN_CONTENT, RegexQNamePattern.MATCH_ALL, Integer.MAX_VALUE, false)) + .thenReturn(childAssocs); when(childAssociationRef.getChildRef()).thenReturn(heldItemRef); + // setup retrying transaction helper + Answer doInTransactionAnswer = new Answer() + { + @SuppressWarnings("rawtypes") + @Override + public Object answer(InvocationOnMock invocation) throws Throwable + { + RetryingTransactionCallback callback = (RetryingTransactionCallback) invocation.getArguments()[0]; + // when(childAssociationRef.getChildRef()).thenReturn(heldItemRef); + return callback.execute(); + } + }; + doAnswer(doInTransactionAnswer).when(mockRetryingTransactionHelper) + . doInTransaction(any(RetryingTransactionCallback.class), anyBoolean(), anyBoolean()); + + when(mockTransactionService.getRetryingTransactionHelper()).thenReturn(mockRetryingTransactionHelper); + patch.applyInternal(); verify(mockNodeService, times(1)).addChild(holdRef, heldItemRef, ASSOC_CONTAINS, PATCH_ASSOC_NAME); } + @SuppressWarnings("unchecked") @Test public void patchRunWithSuccessWhenNoHeldChildren() { when(mockFilePlanService.getFilePlans()).thenReturn(fileplans); when(mockHoldService.getHolds(filePlanRef)).thenReturn(holds); - when(mockNodeService.getChildAssocs(holdRef, ASSOC_FROZEN_CONTENT, RegexQNamePattern.MATCH_ALL)).thenReturn(emptyList()); + when(mockNodeService.getChildAssocs(holdRef, ASSOC_FROZEN_CONTENT, RegexQNamePattern.MATCH_ALL, Integer.MAX_VALUE, false)) + .thenReturn(emptyList()); + + // setup retrying transaction helper + Answer doInTransactionAnswer = new Answer() + { + @SuppressWarnings("rawtypes") + @Override + public Object answer(InvocationOnMock invocation) throws Throwable + { + RetryingTransactionCallback callback = (RetryingTransactionCallback) invocation.getArguments()[0]; + when(childAssociationRef.getChildRef()).thenReturn(heldItemRef); + return callback.execute(); + } + }; + doAnswer(doInTransactionAnswer).when(mockRetryingTransactionHelper) + . doInTransaction(any(RetryingTransactionCallback.class), anyBoolean(), anyBoolean()); + + when(mockTransactionService.getRetryingTransactionHelper()).thenReturn(mockRetryingTransactionHelper); patch.applyInternal(); diff --git a/amps/ags/rm-community/rm-community-rest-api-explorer/pom.xml b/amps/ags/rm-community/rm-community-rest-api-explorer/pom.xml index 3021938725..b09471b0fd 100644 --- a/amps/ags/rm-community/rm-community-rest-api-explorer/pom.xml +++ b/amps/ags/rm-community/rm-community-rest-api-explorer/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-governance-services-community-repo-parent - 17.43-SNAPSHOT + 20.5-SNAPSHOT diff --git a/amps/pom.xml b/amps/pom.xml index 508ba5f731..b75d2b692a 100644 --- a/amps/pom.xml +++ b/amps/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo - 17.43-SNAPSHOT + 20.5-SNAPSHOT diff --git a/amps/share-services/pom.xml b/amps/share-services/pom.xml index 1abb9a55ca..410708dbef 100644 --- a/amps/share-services/pom.xml +++ b/amps/share-services/pom.xml @@ -8,7 +8,7 @@ org.alfresco alfresco-community-repo-amps - 17.43-SNAPSHOT + 20.5-SNAPSHOT diff --git a/amps/share-services/src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/search/live-search-people.get.js b/amps/share-services/src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/search/live-search-people.get.js index 2762cfc6f1..2b60bc5de0 100644 --- a/amps/share-services/src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/search/live-search-people.get.js +++ b/amps/share-services/src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/search/live-search-people.get.js @@ -11,7 +11,7 @@ function main() var params = { type: "people", - term: args.t, + term: args.t + " [hint:useCQ]", maxResults: (args.maxResults !== null) ? parseInt(args.maxResults, 10) : DEFAULT_MAX_RESULTS, startIndex: (args.startIndex !== null) ? parseInt(args.startIndex, 10) : 0 }; diff --git a/amps/share-services/src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/search/search.get.js b/amps/share-services/src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/search/search.get.js index aebd5ac93b..3a188f70e2 100644 --- a/amps/share-services/src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/search/search.get.js +++ b/amps/share-services/src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/search/search.get.js @@ -14,7 +14,7 @@ function main() maxResults: (args.maxResults !== null) ? parseInt(args.maxResults, 10) : DEFAULT_MAX_RESULTS, pageSize: (args.pageSize !== null) ? parseInt(args.pageSize, 10) : DEFAULT_PAGE_SIZE, startIndex: (args.startIndex !== null) ? parseInt(args.startIndex, 10) : 0, - facetFields: args.facetFields, + facetFields: args.facetFields !== null ? args.facetFields.replace( /(<([^>]+)>)/ig, '') : null, filters: args.filters, encodedFilters: args.encodedFilters, spell: (args.spellcheck !== null) ? (args.spellcheck == "true") : false diff --git a/amps/share-services/src/test/java/org/alfresco/slingshot/web/scripts/SlingshotContentGetTest.java b/amps/share-services/src/test/java/org/alfresco/slingshot/web/scripts/SlingshotContentGetTest.java index f500bf0cd2..0069297d67 100644 --- a/amps/share-services/src/test/java/org/alfresco/slingshot/web/scripts/SlingshotContentGetTest.java +++ b/amps/share-services/src/test/java/org/alfresco/slingshot/web/scripts/SlingshotContentGetTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2005 - 2020 Alfresco Software Limited. + * Copyright 2005 - 2022 Alfresco Software Limited. * * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of the paid license agreement will prevail. @@ -40,6 +40,7 @@ import org.json.JSONObject; import org.junit.Assert; import org.springframework.extensions.webscripts.TestWebScriptServer; import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest; +import org.springframework.extensions.webscripts.TestWebScriptServer.Response; import java.io.Serializable; import java.util.ArrayList; @@ -194,13 +195,13 @@ public class SlingshotContentGetTest extends BaseWebScriptTest NodeRef rootFolder = createNode(companyHome, "rootFolder", ContentModel.TYPE_FOLDER); - NodeRef doc1 = createNodeWithTextContent(rootFolder, "doc1", ContentModel.TYPE_CONTENT, "doc1 file content"); + NodeRef doc1 = createNodeWithTextContent(rootFolder, "doc1", ContentModel.TYPE_CONTENT, "doc1 file content", MimetypeMap.MIMETYPE_TEXT_PLAIN); NodeRef folderX = createNode(rootFolder, "X", ContentModel.TYPE_FOLDER); NodeRef folderY = createNode(folderX, "Y", ContentModel.TYPE_FOLDER); NodeRef folderZ = createNode(folderY, "Z", ContentModel.TYPE_FOLDER); - NodeRef doc2 = createNodeWithTextContent(folderZ, "doc2", ContentModel.TYPE_CONTENT, "doc2 file content"); + NodeRef doc2 = createNodeWithTextContent(folderZ, "doc2", ContentModel.TYPE_CONTENT, "doc2 file content", MimetypeMap.MIMETYPE_TEXT_PLAIN); // uri with relative path at the end String uri = URL_CONTENT_DOWNLOAD + doc1.getId() + "/X/Y/Z/doc2"; @@ -212,7 +213,50 @@ public class SlingshotContentGetTest extends BaseWebScriptTest nodeService.deleteNode(rootFolder); } - public NodeRef createNodeWithTextContent(NodeRef parentNode, String nodeCmName, QName nodeType, String content) + public void testForcedAttachment() throws Exception + { + Repository repositoryHelper = (Repository) getServer().getApplicationContext().getBean("repositoryHelper"); + NodeRef companyHome = repositoryHelper.getCompanyHome(); + + NodeRef rootFolder = createNode(companyHome, "rootFolder", ContentModel.TYPE_FOLDER); + NodeRef testhtml = createNodeWithTextContent(rootFolder, "testhtml", ContentModel.TYPE_CONTENT, "testhtml content", MimetypeMap.MIMETYPE_HTML); + NodeRef testpdf = createNodeWithTextContent(rootFolder, "testpdf", ContentModel.TYPE_CONTENT, "testpdf content", MimetypeMap.MIMETYPE_PDF); + + String uri = URL_CONTENT_DOWNLOAD + testhtml.getId() + "?a=false"; + GetRequest req = new GetRequest(uri); + Response res = sendRequest(req, 200); + assertEquals("attachment", res.getHeader("Content-Disposition")); + assertEquals(MimetypeMap.MIMETYPE_HTML + ";charset=UTF-8", res.getContentType()); + + uri = URL_CONTENT_DOWNLOAD + testhtml.getId(); + res = sendRequest(new GetRequest(uri), 200); + assertEquals("attachment", res.getHeader("Content-Disposition")); + assertEquals(MimetypeMap.MIMETYPE_HTML + ";charset=UTF-8", res.getContentType()); + + uri = URL_CONTENT_DOWNLOAD + testhtml.getId() + "?a=true"; + res = sendRequest(new GetRequest(uri), 200); + assertEquals("attachment", res.getHeader("Content-Disposition")); + assertEquals(MimetypeMap.MIMETYPE_HTML + ";charset=UTF-8", res.getContentType()); + + uri = URL_CONTENT_DOWNLOAD + testpdf.getId() + "?a=false"; + res = sendRequest(new GetRequest(uri), 200); + assertNull(res.getHeader("Content-Disposition")); + assertEquals(MimetypeMap.MIMETYPE_PDF + ";charset=UTF-8", res.getContentType()); + + uri = URL_CONTENT_DOWNLOAD + testpdf.getId(); + res = sendRequest(new GetRequest(uri), 200); + assertNull(res.getHeader("Content-Disposition")); + assertEquals(MimetypeMap.MIMETYPE_PDF + ";charset=UTF-8", res.getContentType()); + + uri = URL_CONTENT_DOWNLOAD + testpdf.getId() + "?a=true"; + res = sendRequest(new GetRequest(uri), 200); + assertEquals("attachment", res.getHeader("Content-Disposition")); + assertEquals(MimetypeMap.MIMETYPE_PDF + ";charset=UTF-8", res.getContentType()); + + nodeService.deleteNode(rootFolder); + } + + public NodeRef createNodeWithTextContent(NodeRef parentNode, String nodeCmName, QName nodeType, String content, String mimetype) { NodeRef nodeRef = createNode(parentNode, nodeCmName, nodeType); @@ -220,7 +264,7 @@ public class SlingshotContentGetTest extends BaseWebScriptTest if (content != null) { ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); - writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setMimetype(mimetype); writer.setEncoding("UTF-8"); writer.putContent(content); } diff --git a/core/pom.xml b/core/pom.xml index e1030de42f..ed1b7b136a 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo - 17.43-SNAPSHOT + 20.5-SNAPSHOT diff --git a/core/src/main/java/org/alfresco/error/ExceptionStackUtil.java b/core/src/main/java/org/alfresco/error/ExceptionStackUtil.java index 8ff83c2452..7967722f19 100644 --- a/core/src/main/java/org/alfresco/error/ExceptionStackUtil.java +++ b/core/src/main/java/org/alfresco/error/ExceptionStackUtil.java @@ -25,6 +25,9 @@ package org.alfresco.error; */ public class ExceptionStackUtil { + private static final String JAVASCRIPT_EXCEPTION = "org.mozilla.javascript.JavaScriptException"; + private static final String EXCEPTION_DELIMITER = ":"; + /** * Searches through the exception stack of the given throwable to find any instance * of the possible cause. The top-level throwable will also be tested. @@ -38,10 +41,17 @@ public class ExceptionStackUtil { while (throwable != null) { + Class throwableClass = throwable.getClass(); + + boolean isJavaScriptException = throwableClass.getName().contains(JAVASCRIPT_EXCEPTION); + String throwableMsg = throwable.getMessage() != null ? throwable.getMessage() : ""; + for (Class possibleCauseClass : possibleCauses) { - Class throwableClass = throwable.getClass(); - if (possibleCauseClass.isAssignableFrom(throwableClass)) + String possibleCauseClassName = possibleCauseClass.getName(); + + if (possibleCauseClass.isAssignableFrom(throwableClass) + || (isJavaScriptException && throwableMsg.contains(possibleCauseClassName + EXCEPTION_DELIMITER))) { // We have a match return throwable; diff --git a/core/src/main/java/org/alfresco/httpclient/RequestHeadersHttpClient.java b/core/src/main/java/org/alfresco/httpclient/RequestHeadersHttpClient.java index 78e5d36727..15ddff141c 100644 --- a/core/src/main/java/org/alfresco/httpclient/RequestHeadersHttpClient.java +++ b/core/src/main/java/org/alfresco/httpclient/RequestHeadersHttpClient.java @@ -21,6 +21,7 @@ package org.alfresco.httpclient; import java.io.IOException; import java.util.Map; +import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HostConfiguration; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpException; @@ -57,7 +58,12 @@ public class RequestHeadersHttpClient extends HttpClient if (defaultHeaders != null) { defaultHeaders.forEach((k,v) -> { - method.addRequestHeader(k, v); + Header h = method.getRequestHeader(k); + boolean add = h == null || h.getValue() == null || !h.getValue().equals(v); + if (add) + { + method.addRequestHeader(k, v); + } }); } } diff --git a/data-model/pom.xml b/data-model/pom.xml index 8dda2b31cb..1029ebf468 100644 --- a/data-model/pom.xml +++ b/data-model/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo - 17.43-SNAPSHOT + 20.5-SNAPSHOT @@ -134,7 +134,7 @@ com.fasterxml.woodstox woodstox-core - 6.3.0 + 6.3.1 diff --git a/mmt/pom.xml b/mmt/pom.xml index 51dad169df..2298321be3 100644 --- a/mmt/pom.xml +++ b/mmt/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo - 17.43-SNAPSHOT + 20.5-SNAPSHOT diff --git a/packaging/distribution/pom.xml b/packaging/distribution/pom.xml index bc18cbc989..b45cef262f 100644 --- a/packaging/distribution/pom.xml +++ b/packaging/distribution/pom.xml @@ -9,6 +9,6 @@ org.alfresco alfresco-community-repo-packaging - 17.43-SNAPSHOT + 20.5-SNAPSHOT diff --git a/packaging/docker-alfresco/Dockerfile b/packaging/docker-alfresco/Dockerfile index 68330df346..e780ffb237 100644 --- a/packaging/docker-alfresco/Dockerfile +++ b/packaging/docker-alfresco/Dockerfile @@ -1,6 +1,6 @@ -# Fetch image based on Tomcat 9.0, Java 11 and Centos 7 +# Fetch image based on Tomcat 9.0, Java 17 and Rocky Linux 8 # More infos about this image: https://github.com/Alfresco/alfresco-docker-base-tomcat -FROM alfresco/alfresco-base-tomcat:tomcat9-jre11-centos7-202203091924 +FROM alfresco/alfresco-base-tomcat:tomcat9-jre17-rockylinux8-202209261711 # Set default docker_context. ARG resource_path=target @@ -65,12 +65,12 @@ RUN sed -i -e "s_log4j.appender.File.File\=alfresco.log_log4j.appender.File.File # fontconfig is required by Activiti worflow diagram generator # installing pinned dependencies as well -RUN yum install -y fontconfig-2.13.0-4.3.el7 \ - dejavu-fonts-common-2.33-6.el7 \ - fontpackages-filesystem-1.44-8.el7 \ - freetype-2.8-14.el7_9.1 \ - libpng-1.5.13-8.el7 \ - dejavu-sans-fonts-2.33-6.el7 && \ +RUN yum install -y fontconfig-2.13.1-4.el8 \ + dejavu-fonts-common-2.35-7.el8 \ + fontpackages-filesystem-1.44-22.el8 \ + freetype-2.9.1-4.el8_3.1 \ + libpng-1.6.34-5.el8 \ + dejavu-sans-fonts-2.35-7.el8 && \ yum clean all # The standard configuration is to have all Tomcat files owned by root with group GROUPNAME and whilst owner has read/write privileges, diff --git a/packaging/docker-alfresco/pom.xml b/packaging/docker-alfresco/pom.xml index 6905858e0f..29e7daacf5 100644 --- a/packaging/docker-alfresco/pom.xml +++ b/packaging/docker-alfresco/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo-packaging - 17.43-SNAPSHOT + 20.5-SNAPSHOT diff --git a/packaging/pom.xml b/packaging/pom.xml index 19b1b4f93e..b000348834 100644 --- a/packaging/pom.xml +++ b/packaging/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo - 17.43-SNAPSHOT + 20.5-SNAPSHOT diff --git a/packaging/tests/environment/.env b/packaging/tests/environment/.env index ca3c68b5dc..c6c00a9d5d 100644 --- a/packaging/tests/environment/.env +++ b/packaging/tests/environment/.env @@ -1,3 +1,3 @@ -SOLR6_TAG=2.0.3 -POSTGRES_TAG=13.3 -ACTIVEMQ_TAG=5.16.1 +SOLR6_TAG=2.0.5 +POSTGRES_TAG=14.4 +ACTIVEMQ_TAG=5.17.1-jre11-rockylinux8 diff --git a/packaging/tests/pom.xml b/packaging/tests/pom.xml index 7a1250d88a..97a2436d63 100644 --- a/packaging/tests/pom.xml +++ b/packaging/tests/pom.xml @@ -6,7 +6,7 @@ org.alfresco alfresco-community-repo-packaging - 17.43-SNAPSHOT + 20.5-SNAPSHOT diff --git a/packaging/tests/scripts/output_logs_for_failures.sh b/packaging/tests/scripts/output_logs_for_failures.sh new file mode 100755 index 0000000000..4fb71d2050 --- /dev/null +++ b/packaging/tests/scripts/output_logs_for_failures.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +TAS_DIRECTORY=$1 + +cd ${TAS_DIRECTORY} + +failures=$(grep 'status="FAIL"' target/surefire-reports/testng-results.xml | sed 's|^.*[ ]name="\([^"]*\)".*$|\1|g') + +for failure in ${failures} +do + cat target/reports/alfresco-tas.log | sed '/STARTING Test: \['${failure}'\]/,/ENDING Test: \['${failure}'\]/!d;/ENDING Test: \['${failure}'\]/q' +done diff --git a/packaging/tests/tas-cmis/README.md b/packaging/tests/tas-cmis/README.md new file mode 100644 index 0000000000..aa099eab6a --- /dev/null +++ b/packaging/tests/tas-cmis/README.md @@ -0,0 +1,495 @@ +![in progress](https://img.shields.io/badge/Document_Level-In_Progress-yellow.svg?style=flat-square) + +:paw_prints: Back to [TAS Master Documentation](https://gitlab.alfresco.com/tas/documentation/wikis/home) + +--- +## Table of Contents +* [Synopsis](#synopsis) +* [Prerequisite](#prerequisite) +* [Installation](#installation-if-you-want-to-contribute) +* [Package Presentation](#package-presentation) +* [Sample Usage](#sample-usage) + * [How to write a test](#how-to-write-a-test) + * [How to run tests?](#how-to-run-tests) + * [from IDE](#from-ide) + * [from command line](#from-command-line) + * [Perform CMIS Queries](#perform-cmis-queries) +* [Listeners](#listeners) +* [Test Results](#test-results) +* [Test Rail Integration](#test-rail-integration) + * [Configuration](#configuration) + * [How to enable Test Rail Integration?](#how-to-enable-test-rail-integration) +* [Change Log](docs/CHANGELOG.md) :glowing_star: +* [Reference](#reference) +* [Releasing](#releasing) +* [Contributors](#contributors) +* [License](#license) + +## Synopsis + +**TAS**( **T**est **A**utomation **S**ystem)- **CMIS** is the project that handles the automated tests related only to CMIS API integrated with Alfresco One [Alfresco CMIS API](http://docs.alfresco.com/5.1/pra/1/topics/cmis-welcome.html). + +It is based on Apache Maven, compatible with major IDEs and is using also Spring capabilities for dependency injection. + +As a high level overview, this project makes use of the following functionality useful in automation testing as: +* reading/defining test environment settings (e.g. alfresco server details, authentication, etc.) +* managing resource (i.e. creating files and folders) +* test data generators (for site, users, content, etc) +* helpers (i.e. randomizers, test environment information) +* test logging generated on runtime and test reporting capabilities +* test management tool integration (at this point we support integration with [Test Rail](https://alfresco.testrail.net) (v5.2.1) +* health checks (verify if server is reachable, if server is online) +* generic Internal-DSL (Domain Specific Language) + +Using Nexus -Release Repository, everyone will be able to use individual interfaces in their projects by extending the automation core functionalities. + +**[Back to Top ^](#table-of-contents)** + +## Prerequisite +(tested on unix/non-unix distribution) +* [Java SE 1.8](http://www.oracle.com/technetwork/java/javase/downloads/index.html). +* [Maven 3.3](https://maven.apache.org/download.cgi) installed and configure according to [Windows OS](https://maven.apache.org/guides/getting-started/windows-prerequisites.html) or [Mac OS](https://maven.apache.org/install.html). +* Configure Maven to use Alfresco alfresco-internal repository following this [Guide](https://ts.alfresco.com/share/page/site/eng/wiki-page?title=Maven_Setup). +* Your favorite IDE as [Eclipse](https://eclipse.org/downloads/) or [IntelliJ](https://www.jetbrains.com/idea). +* Access to [Nexus](https://nexus.alfresco.com/nexus/) repository. +* Access to GitLab [TAS](https://gitlab.alfresco.com/tas/) repository. +* GitLab client for your operating system. (we recommend [SourceTree](https://www.sourcetreeapp.com) - use your google account for initial setup). +* Getting familiar with [Basic Git Commands](http://docs.gitlab.com/ee/gitlab-basics/basic-git-commands.html). +* Getting familiar with [Maven](https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html). +* Getting familiar with [Spring](http://docs.spring.io). +* Getting familiar with [TestNG](http://testng.org/doc/index.html) + +**[Back to Top ^](#table-of-contents)** + +## Installation (if you want to contribute) + +* Open your GitLab client and clone the repository of this project. +* You can do this also from command line (or in your terminal) adding: + +```bash +$ git clone https://gitlab.alfresco.com/tas/alfresco-tas-cmis-test.git +# this clone will have the latest changes from repository. If you want to checkout a specific version released, take a look at the [Change Log](docs/CHANGELOG.md) page +$ cd alfresco-tas-cmis-test +# this command will checkout the remove v1.0.0 tagged repository and create locally a new branch v1.0.0 +$ git checkout tags/v1.0.0 -b v1.0.0 +``` + +* Install and check if all dependencies are downloaded + +```bash +$ mvn clean install -DskipTests +# you should see one [INFO] BUILD SUCCESS message displayed +``` +**[Back to Top ^](#table-of-contents)** + +## Package Presentation + +The project uses a maven layout [archetype](https://maven.apache.org/plugins-archives/maven-archetype-plugin-1.0-alpha-7/examples/simple.html): +```ruby +├── pom.xml +├── src +│   ├── main +│   │   └── java +│   │   └── org +│   │   └── alfresco +│   │   └── cmis +│   │   ├── (...) +│   │   ├── CmisProperties.java #handles all properties from default.properties +│   │   ├── CmisWrapper.java #wrapper around CMIS API +│   │   └── exception +│   │   └── (...) +│   ├── test +│   │   ├── java +│   │   │   └── org +│   │   │   └── alfresco +│   │   │   └── cmis +│   │   │   ├── CmisDemoTests.java #demo example +│   │   │   └── CmisTest.java #abstract base class that should be inherited by all tests +│   │   └── resources +│   │   ├── alfresco-cmis-context.xml #spring configuration +│   │   ├── default.properties #all settings related to environment, protocol +│   │   ├── log4j.properties +│   │   └── sanity-cmis.xml # default suite of tests +``` + +**[Back to Top ^](#table-of-contents)** + +## Sample Usage + +Following the standard layout for Maven projects, the application sources locate in src/main/java and test sources locate in src/test/java. +Application sources consist in defining the CMIS object that simulates the API calls. +The tests are based on an abstract object: CmisTest.java that handles the common behavior: checking the health status of the test server, configuration settings, getting the general properties, etc. + +Please take a look at [CmisDemoTests.java](src/test/java/org/alfresco/cmis/CmisDemoTests.java) class for an example. + +Common configuration settings required for this project are stored in properties file, see [default.properties](src/test/resources/default.properties). +Please analyze and update it accordingly with Alfresco test server IP, port, credentials, etc. + +Example: +```java +# Alfresco HTTP Server Settings +alfresco.scheme=http +alfresco.server= +alfresco.port= +``` + +* optional update the logging level in [log4j](src/test/resources/log4j.properties) file (you can increase/decrease the deails of the [logging file](https://logging.apache.org/log4j/1.2/manual.html), setting the ```log4j.rootLogger=DEBUG``` if you want.) +* go to [running](#how-to-run-tests) section for more information on how to run this tests. + +**[Back to Top ^](#table-of-contents)** + +### How to write a test + +* Tests are organized in java classes and located on src/test/java as per maven layout. +* One test class should contain the tests that cover one functionality as we want to have a clear separation of test scope: tests for sanity/core/full, tests that verify manage of folder/files etc. +* These are the conventions that need to follow when you write a test: + * The test class has @Test annotation with the group defined: protocols, cmis. You can add more groups like sanity, regression + + ```java + @Test(groups={ "sanity"} + ``` + + * The test has @TestRail annotation in order to assure that the details and results will be submitted on TestRail. The fields for TestRail annotation will be explained on next chapter. + + + ```java + @TestRail(section = { "cmis-api" }, executionType=ExecutionType.SANITY, + description = "Verify admin user creates folder in DocumentLibrary with CMIS") + public void adminShouldCreateFolderInSite() throws Exception + { cmisApi.usingSite(testSite).createFolder(testFolder).assertExistsInRepo(); } + + ``` + + * Use Spring capabilities to initialize the objects(Models, Wrappers) with @Autowired + * We followed Builder pattern to develop specific DSL for simple and clear usage of protocol client in test: + + ```java + cmisApi.usingSite(testSite) .createFolder(testFolder) .assertExistsInRepo(); + ``` + * To view a simple class that is using this utility, just browse on [CmisDemoTests.java](src/test/java/org/alfresco/cmis/CmisDemoTests.java) + Notice the class definition and inheritance value: + + ```java + public class CmisDemoTests extends CmisTest + ``` + + * as a convention, before running your test, check if the test environment is reachable and your alfresco test server is online. + (this will stop the test if the server defined in your property file is not healthy - method available in parent class) + + ```java + @BeforeClass(alwaysRun = true) + public void setupCmisTest() throws Exception { + serverHealth.assertServerIsOnline(); + } + ``` + * the test name are self explanatory: + + ```java + @TestRail(section = { "cmis-api" }, executionType=ExecutionType.SANITY, description = "Verify admin user creates folder in DocumentLibrary with CMIS") + public void adminShouldCreateFolderInSite() throws Exception + { + cmisApi.usingSite(testSite) + .createFolder(testFolder) + .assertExistsInRepo(); + } + ``` + + ```java + @TestRail(section = { "cmis-api" }, executionType=ExecutionType.SANITY, description = "Verify admin user creates and renames folder in DocumentLibrary with CMIS") + public void adminShouldRenameFolderInSite() throws Exception + { + cmisApi.usingSite(testSite).createFolder(testFolder) + .and().rename("renamed") + .assertExistsInRepo(); + } + ``` + +**[Back to Top ^](#table-of-contents)** + +### How to run tests + +#### from IDE + +* The project can be imported into a development environment tool (Eclipse or IntelliJ). You have the possibility to execute tests or suite of tests using [TestNG plugin](http://testng.org/doc/eclipse.html) previously installed in IDE. + From Eclipse, just right click on the testNG class (something similar to [CmisDemoTests.java](src/test/java/org/alfresco/cmis/CmisDemoTests.java)), select Run As - TestNG Test + You should see your test passed. + +* In case you are using the default settings that points to localhost (127.0.0.1) and you don't have Alfresco installed on your machine, you will see one exception thrown (as expected): + ```java + org.alfresco.utility.exception.ServerUnreachableException: Server {127.0.0.1} is unreachable. + ``` + +#### from command line + +* In terminal or CMD, navigate (with CD) to root folder of your project (you can use the sample project): + + + + The tests can be executed on command line/terminal using Maven command + + ```bash + mvn test + ``` + + This command with trigger the tests specified in the default testNG suite from POM file: src/main/resources/shared-resources/cmis-suites.xml + + You can use -Dtest parameter to run the test/suites through command line (http://maven.apache.org/surefire/maven-surefire-plugin/examples/single-test.html). + + You can also specify a different suiteXMLFile like: + + ```bash + mvn test -DsuiteXmlFile=src/resources/your-custom-suite.xml + ``` + + Or even a single test: + + ```bash + mvn test -Dtest=org.alfresco.cmis.CmisDemoTests + ``` + But pay attention that you will not have enabled all the [listeners](#listeners) in this case (the Reporting listener or TestRail integration one) + +### Perform CMIS Queries +(:glowing_star: please notice that at this point we assert only the results count returned by the query: we plan to extend the functionality to assert on QueryResult iterable objects also: simple modification on [QueryExecutor.java](src/main/java/org/alfresco/cmis/dsl/QueryExecutor.java) + +There are a couple of ways to test the results count after performing CMIS queries, choose the one that you like the most: + +a) direct queries using a simple TestNG test: + +(see example [here](src/test/java/org/alfresco/cmis/search/SorlSearchSimpleQueryTests.java)) +```java +public class SorlSearchSimpleQueryTests extends CmisTest +{ + @Test + public void simpleQueryOnFolderDesc() throws Exception + { + // create here multiple folder as data preparation + cmisApi.authenticateUser(dataUser.getAdminUser()) + .withQuery("SELECT * FROM cmis:folder ORDER BY cmis:createdBy DESC").assertResultsCount().isLowerThan(101); + } +} +``` +- just extend CmisTest +- authenticate with your UserModel and perform the query. The DSL will allow you to assert the result count if is equal, lower or greater than to a particular value. You can update the methods in [QueryResultAssertion](src/main/java/org/alfresco/cmis/dsl/QueryExecutor.java) class. + +b) define one set of test data (folders, files, etc. ) that you will search in all tests then execute all CMIS queris from one common XML file +- see test class [SolrSearchInFolderTests](src/test/java/org/alfresco/cmis/search/SolrSearchInFolderTests.java) +- see [XML test data](src/main/resources/shared-resources/testdata/search-in-folder.xml) used in [SolrSearchInFolderTests](src/test/java/org/alfresco/cmis/search/SolrSearchInFolderTests.java) into one DataProvider. Notice that XML file has two parameter: the query that will be executed and the expected result count returned. + +c) define test data (user, sites, folder, files, aspects, comments, custom models, etc) all into one XML file with all cmis queries related. +- see example on [SolrSearchByIdTests](https://gitlab.alfresco.com/tas/alfresco-tas-cmis-test/blob/master/src/test/java/org/alfresco/cmis/search/SolrSearchByIdTests.java) +- notice the 'NODE_REF[x]'; 'NODE_REF[y]' keywords that will dynamically take the test data identified by id: x, y (you will figure it out based on examples). + +**Info**: all search test queries are found [org.alfresco.cmis.search](src/test/java/org/alfresco/cmis/search) package. + +**[Back to Top ^](#table-of-contents)** + +## Listeners + + With the help of Listeners we can modify the behaviour of TestNG framework. There are a lot of testNG listener interfaces that we can override in order to provide new functionalities. + The tas framework provides out of the box a couple of listeners that you could use. These could be enabled and added at the class level or suite level. + +### a) org.alfresco.utility.report.ReportListenerAdapter + + * if added at the class level: + + ```java + @Listeners(value=ReportListenerAdapter.class) + public class MyTestClass extends CmisTest + { + (...) + } + ``` + + * or suite xml level + + ```java + + + + + (...) + + ``` + It will automatically generate one html named "report.html" in ./target/report folder. + Please also take a look at [Test Results](#test-results) section. + +### b) org.alfresco.utility.testrail.TestRailExecutorListener + It will automatically update Test Rail application with the test cases that you've automated. + Please take a look at [Test Rail Integration](#test-rail-integration) section for more details. + +### c) org.alfresco.utility.report.log.LogsListener +This is a new listener that will generate further details in one XML format of the automated test steps that you will write. + +Example: + +```java +public void myDSLMethod1() +{ + STEP("Lorem ipsum dolor sit amet"); + //code for first step + + STEP("consectetur adipiscing elit"); + //code for the next description +} + +public void myDSLMethod2() +{ + STEP("sed do eiusmod tempor incididunt ut labore"); + //code for first step + + STEP("et dolore magna aliqua"); + //code for the next description +} +``` + +If these methods will be executed insite a test method, all those steps will be automatically logged in the XML report generated. +Example: + +```java +@Test +public void adminShouldCreateFileInSite() +{ + myDSLMethod1(); + myDSLMethod2() +} +``` + +So if "testingSomething" will be executed this is what you will see on the XML file generated. (please take a look at [Test Results](#test-results) section for defining the defaul location) + +Here is one example of XML file generated with these steps: + +![](docs/pics/xml-steps-report.JPG) + +**[Back to Top ^](#table-of-contents)** + +## Test Results + We already executed a couple of tests using command line as indicated above. Sweet! Please take a look at [sanity-cmis.xml](src/test/resources/sanity-cmis.xml) one more time. + You will see there that we have one listener added: + + ```java + + ``` + This will tell our framework, after we run all tests, to generate one HTML report file with graphs and metrics. + + Take a look at the target/reports folder (created after running the tests) and open the report.html file. + + ![](docs/pics/html-report-sample.JPG) + + Playing with this report, you will notice that you will be able to: + * search tests cases by name + * filter test cases by errors, labels, groups, test types, date when it was executed, protocol used, etc. + * view overall pass/fail metrics of current test suite, history of tests execution, etc. + + The report path can be configured in default.properties): + + ``` + # The location of the reports path + reports.path=your-new-location-of-reports + ``` + +**[Back to Top ^](#table-of-contents)** + +## Test Rail Integration + +Alfresco is using now https://alfresco.testrail.net (v5.3.0.3601). + +We aim to accelerate the delivery of automated test by minimizing the interaction with the test management tool - TestRail. In this scope we developed the following capabilities: +* creating automatically the manual tests in TestRail +* submitting the test results (with stack trace) after each execution into TestRail Test Runs +* adding the test steps for each test. + +### Configuration +In order to use Test Rail Integration you will need to add a couple of information in [default.properties](src/test/resources/default.properties) file: +(the document is pretty self explanatory) + +```java +# Example of configuration: +# ------------------------------------------------------ +# testManagement.endPoint=https://alfresco.testrail.com/ +# testManagement.username= +# testManagement.apiKey= +# testManagement.project= +``` +!This settings are already defined in default.properties for you. + + +For generating a new API Key take a look at the official documentation, TestRail [APIv2](http://docs.gurock.com/testrail-api2) +* _testManagement.project= **_" this represents the name of the Test Run from your project. +* In Test Rail, navigating to Test Runs & Results, create a new Test Run and include all/particular test cases. If this test run name is "Automation", update _testManagement.testRun= **Automation**_. + All test results will be updated only on this test run at runtime as each test is executed by TAS framework. + +### How to enable Test Rail Integration? + +We wanted to simplify the Test Rail integration, so we used listeners in order to enable/disable the integration of Test Rail. +* first configure your default.properties as indicated above + +* now on your TestNG test, add the @TestRail annotation, so let's say you will have this test: + + ```java + @Test(groups="sample-tests") + public void thisAutomatedTestWillBePublishedInTestRail() + { + } + ``` + add now @TestRail integration with mandatory field ```section```. This means that this tests annotated, will be uploaded in TestRail: + + ```java + @Test(groups="sample-tests") + @TestRail(section = { "protocols", "TBD" }) + public void thisAutomatedTestWillBePublishedInTestRail() + { + } + ``` + The section field, represents an array of strings, the hierarchy of sections that SHOULD be found on TestRail under the project you've selected in default.properties. Follow the TestRail [user-guide](http://docs.gurock.com/testrail-userguide/start) for more information regarding sections. + In our example we created in Test Rail one root section "protocols" with a child section: "TBD" (you can go further and add multiple section as you wish) + +* now, lets add the listener, the TestRailExecutorListener that will handle this TC Management interaction. + This listener can be added at the class level or suite level (approach that we embrace) + Take a look at [sanity-cmis.xml](src/test/resources/sanity-cmis.xml) for further example. + + ```xml + + + (...) + + ``` + + Right click on cmis-suites.xml file and run it, or just "mvn test" from root if this sample project. + After everything passes, go in Test Rail, open your project and navigate to "Test Cases" section. Notice that under protocols/TBD section, you will see your test case published. + + If you defined also the "testManagement.testRun" correctly, you will see under Test Runs, the status of this case marked as passed. + + The @TestRail annotation offers also other options like: + - "description" this is the description that will be updated in Test Rail for your test case + - "testType", the default value is set to Functional test + - "executionType", default value is set to ExecutionType.REGRESSION, but you can also use ExecutionType.SMOKE, ExecutionType.SANITY, etc + + Take a look at the demo scenarios in this project for further examples. + +**[Back to Top ^](#table-of-contents)** + +## Reference + +* For any improvements, bugs, please use Jira - [TAS](https://issues.alfresco.com/jira/browse/TAS) project. +* Setup the environment using [docker](https://gitlab.alfresco.com/tas/alfresco-docker-provisioning/blob/master/Readme.md). +* [Bamboo Test Plan](https://bamboo.alfresco.com/bamboo/browse/TAS-CMIS) + +## Contributors + +As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other... [more](CODE_OF_CONDUCT.md) + +## Releasing + +Any commit done on this project should be automatically executed by [TAS Build Plan](https://bamboo.alfresco.com/bamboo/browse/TAS-TAS) +If the build passes, then you didn't broke anything. + +If you want to perform a release, open [TAS-CMIS](https://bamboo.alfresco.com/bamboo/browse/TAS-CMIS) Bamboo Build. +Run the Default stage and if it passes, then manually perform the Release stage (this will auto-increment the version in pom.xml) + +## License + +TBD diff --git a/packaging/tests/tas-cmis/docs/CHANGELOG.md b/packaging/tests/tas-cmis/docs/CHANGELOG.md new file mode 100644 index 0000000000..a873dd833e --- /dev/null +++ b/packaging/tests/tas-cmis/docs/CHANGELOG.md @@ -0,0 +1,20 @@ +:paw_prints: Back to Utility [README](README.md). + +--- +# Change Log +All notable changes to this project will be documented in this file. + +Each tag bellow has a corresponded version released in [Nexus](https://nexus.alfresco.com/nexus/#welcome). +Currently we are testing [CMIS v1.1](http://docs.oasis-open.org/cmis/CMIS/v1.1/CMIS-v1.1.html) with Alfresco One. + +(if you need to update/polish tests please branch from the release tags) + +## [[v5.2.0-1] - 2016-12-12](/tas/alfresco-tas-cmis-test/commits/v5.2.0-1) +### TBD + +## [[v5.2.0-0] - 2016-12-12](/tas/alfresco-tas-cmis-test/commits/v5.2.0-0) +- works with 5.2 alfresco +- 100% Core tests for CMIS +- 100% Sanity test for CMIS +- use released v1.0.7 utility + diff --git a/packaging/tests/tas-cmis/docs/pics/html-report-sample.JPG b/packaging/tests/tas-cmis/docs/pics/html-report-sample.JPG new file mode 100644 index 0000000000..d03f6e2086 Binary files /dev/null and b/packaging/tests/tas-cmis/docs/pics/html-report-sample.JPG differ diff --git a/packaging/tests/tas-cmis/docs/pics/html-report-sample.png b/packaging/tests/tas-cmis/docs/pics/html-report-sample.png new file mode 100644 index 0000000000..576422e34f Binary files /dev/null and b/packaging/tests/tas-cmis/docs/pics/html-report-sample.png differ diff --git a/packaging/tests/tas-cmis/docs/pics/xml-steps-report.JPG b/packaging/tests/tas-cmis/docs/pics/xml-steps-report.JPG new file mode 100644 index 0000000000..e670430aaf Binary files /dev/null and b/packaging/tests/tas-cmis/docs/pics/xml-steps-report.JPG differ diff --git a/packaging/tests/tas-cmis/pom.xml b/packaging/tests/tas-cmis/pom.xml index 885d89b184..655500f1c9 100644 --- a/packaging/tests/tas-cmis/pom.xml +++ b/packaging/tests/tas-cmis/pom.xml @@ -1,27 +1,27 @@ - + 4.0.0 org.alfresco.tas - alfresco-community-repo-cmis-test - cmis test - jar - + cmis + alfresco-tas-cmis org.alfresco alfresco-community-repo-tests - 17.43-SNAPSHOT + 20.5-SNAPSHOT - - - Paul Brodner - - Test Automation Architect - - - + + Alfresco Software + http://www.alfresco.com/ + + 11 + UTF-8 + 1.1.0 + 3.1.1 + 2.5.3 + 11 ${project.basedir}/src/test/resources/cmis-suite.xml @@ -58,10 +58,29 @@ + + org.jboss.resteasy + resteasy-jackson2-provider + 4.7.1.Final + + + org.alfresco.tas - cmis - test + utility + + + mysql + mysql-connector-java + + + + + + + org.apache.chemistry.opencmis + chemistry-opencmis-commons-api + ${chemistry-opencmis-commons-api} diff --git a/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/AuthParameterProviderFactory.java b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/AuthParameterProviderFactory.java new file mode 100644 index 0000000000..d6b9202f77 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/AuthParameterProviderFactory.java @@ -0,0 +1,130 @@ +package org.alfresco.cmis; + +import org.alfresco.utility.data.AisToken; +import org.alfresco.utility.data.auth.DataAIS; +import org.alfresco.utility.model.UserModel; +import org.apache.chemistry.opencmis.commons.SessionParameter; +import org.keycloak.authorization.client.util.HttpResponseException; +import org.keycloak.representations.AccessTokenResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import static org.alfresco.utility.report.log.Step.STEP; + +@Service +public class AuthParameterProviderFactory +{ + public static String STEP_PREFIX = "CMIS AuthParameterProvider:"; + + @Autowired + private DataAIS dataAIS; + + @Autowired + private CmisProperties cmisProperties; + + /** + * + * The default provider uses AIS if support for Alfresco Identity Service is enabled. + * Otherwise a provider which uses Basic authentication is returned. + * + * @return Function which takes a {@link UserModel} and returns a map of + * authentication parameters to be used with {@link CmisWrapper#authenticateUser(UserModel, Function)} + */ + public Function> getDefaultProvider() + { + if (dataAIS.isEnabled()) + { + STEP(String.format("%s Retrieved default AIS auth parameter provider.", STEP_PREFIX)); + return new AisAuthParameterProvider(); + } + else + { + STEP(String.format("%s Retrieved default Basic auth parameter provider.", STEP_PREFIX)); + return new BasicAuthParameterProvider(); + } + } + + public Function> getAISProvider() + { + return new AisAuthParameterProvider(); + } + + public Function> getBasicProvider() + { + return new BasicAuthParameterProvider(); + } + + private class BasicAuthParameterProvider implements Function> + { + @Override + public Map apply(UserModel userModel) + { + STEP(String.format("%s Using Basic auth parameter provider.", STEP_PREFIX)); + Map parameters = new HashMap<>(); + parameters.put(SessionParameter.USER, userModel.getUsername()); + parameters.put(SessionParameter.PASSWORD, userModel.getPassword()); + return parameters; + } + } + + private class AisAuthParameterProvider implements Function> + { + @Override + public Map apply(UserModel userModel) + { + Map parameters = new HashMap<>(); + + STEP(String.format("%s Using AIS auth parameter provider.", STEP_PREFIX)); + AisToken aisToken = getAisAccessToken(userModel); + + parameters.put(SessionParameter.AUTHENTICATION_PROVIDER_CLASS, "org.apache.chemistry.opencmis.client.bindings.spi.OAuthAuthenticationProvider"); + parameters.put(SessionParameter.OAUTH_ACCESS_TOKEN, aisToken.getToken()); + parameters.put(SessionParameter.OAUTH_REFRESH_TOKEN, aisToken.getRefreshToken()); + parameters.put(SessionParameter.OAUTH_EXPIRATION_TIMESTAMP, String.valueOf(System.currentTimeMillis() + + (aisToken.getExpiresIn() * 1000))); // getExpiresIn is in seconds + parameters.put(SessionParameter.OAUTH_TOKEN_ENDPOINT, cmisProperties.aisProperty().getAdapterConfig().getAuthServerUrl() + + "/realms/alfresco/protocol/openid-connect/token"); + parameters.put(SessionParameter.OAUTH_CLIENT_ID, cmisProperties.aisProperty().getAdapterConfig().getResource()); + return parameters; + } + + /** + * Returns a valid access token for valid user credentials in userModel. + * An invalid access token is returned for invalid user credentials, + * which can be used for tests involving non existing or unauthorized users. + * @param userModel + * @return + */ + private AisToken getAisAccessToken(UserModel userModel) + { + String badToken = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJUazFPZ2JqVlo1UEw2bmtsNWFvTUlacTZ4cW9PZzc5WGtzdnJTTUcxLUFZIn0.eyJqdGkiOiI3NTVkMGZiOS03NzI5LTQ1NzYtYWM4Ny1hZWZjZWNiZDE0ZGEiLCJleHAiOjE1NTM2MjQ1NDgsIm5iZiI6MCwiaWF0IjoxNTUzNjI0MjQ4LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0L2F1dGgvcmVhbG1zL2FsZnJlc2NvIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6Ijk4NDE0Njg4LTUwMDUtNDVmOS05YTVjLTlkMDRlODMyYTNkMiIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFsZnJlc2NvIiwiYXV0aF90aW1lIjowLCJzZXNzaW9uX3N0YXRlIjoiNjJlN2U5YzktZmFlNS00N2RhLTk5MDItMTZjYTJhZWUwMWMwIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwOi8vbG9jYWxob3N0KiIsImh0dHBzOi8vbG9jYWxob3N0KiJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoidXNlci12eGlrcXd3cG5jYmpzeHgifQ.PeLGCNCzj-P2m0knwUU9Vfx4dzLLQER9IdV7GyLel9LRN-3J9nh7GBDRQsyDJ0pqhObQyMg4V3wSsrsXRQ6gKhmUyDemmD-w1YMC2a2HKX6GlxsTEF_f1K_R15lIQOawNVErlWjZWORJGCvCYZOJ99SOmeOC6PGY79zLL94MMnf6dXcegePPMOKG-59eNjBkOylTipYebvM40nbbKrS5vzNHQlvUh4ALFeBoMSKGnLSjQd06Dj4SWojG0p1BrxurqDjW0zz6pQlEAm4vcWApRZ6qBLZcMH8adYix07zCDb87GOn1pmfEBWpwd3BEgC_LLu06guaCPHC9tpeIaDTHLg"; + String badRefreshToken = "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJmM2YyMjhjYS1jMzg5LTQ5MGUtOGU1Zi02YWI1MmJhZDVjZGEifQ.eyJqdGkiOiIyNmExZWNhYy00Zjk0LTQwYzctYjJjNS04NTlhZmQ3NjBiYWMiLCJleHAiOjE1NTM2MjYwNDgsIm5iZiI6MCwiaWF0IjoxNTUzNjI0MjQ4LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0L2F1dGgvcmVhbG1zL2FsZnJlc2NvIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdC9hdXRoL3JlYWxtcy9hbGZyZXNjbyIsInN1YiI6Ijk4NDE0Njg4LTUwMDUtNDVmOS05YTVjLTlkMDRlODMyYTNkMiIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJhbGZyZXNjbyIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6IjYyZTdlOWM5LWZhZTUtNDdkYS05OTAyLTE2Y2EyYWVlMDFjMCIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGVtYWlsIn0.lRBJQc7tj0rk7JBC0zpM0dDdZgDKjm9wcxP8nzLnXe4"; + + AisToken aisToken; + try + { + // Attempt to get an access token for userModel from AIS + aisToken = dataAIS.perform().getAccessToken(userModel); + } + catch (HttpResponseException e) + { + // Trying to authenticate with invalid user credentials so return an invalid access token + if (e.getStatusCode() == 401) + { + STEP(String.format("%s Invalid user credentials were provided %s:%s. Using invalid token for reqest.", + STEP_PREFIX, userModel.getUsername(), userModel.getPassword())); + aisToken = new AisToken(badToken, badRefreshToken, System.currentTimeMillis(), 300000); + } + else + { + throw e; + } + } + return aisToken; + } + } +} diff --git a/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/CmisProperties.java b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/CmisProperties.java new file mode 100644 index 0000000000..033f2f7ab4 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/CmisProperties.java @@ -0,0 +1,64 @@ +package org.alfresco.cmis; + +import org.alfresco.utility.TasAisProperties; +import org.alfresco.utility.TasProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; + +@Configuration +@PropertySource("classpath:default.properties") +@PropertySource(value = "classpath:${environment}.properties", ignoreResourceNotFound = true) +public class CmisProperties +{ + @Autowired + private TasProperties properties; + + @Autowired + private TasAisProperties aisProperties; + + public TasProperties envProperty() + { + return properties; + } + + public TasAisProperties aisProperty() + { + return aisProperties; + } + + @Bean + public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() + { + return new PropertySourcesPlaceholderConfigurer(); + } + + @Value("${cmis.binding}") + private String cmisBinding; + + @Value("${cmis.basePath}") + private String basePath; + + public String getCmisBinding() + { + return cmisBinding; + } + + public String getBasePath() + { + return basePath; + } + + public void setBasePath(String basePath) + { + this.basePath = basePath; + } + + public void setCmisBinding(String cmisBinding) + { + this.cmisBinding = cmisBinding; + } +} diff --git a/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/CmisWrapper.java b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/CmisWrapper.java new file mode 100644 index 0000000000..10407b9a5c --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/CmisWrapper.java @@ -0,0 +1,1126 @@ +package org.alfresco.cmis; + +import org.alfresco.cmis.dsl.BaseObjectType; +import org.alfresco.cmis.dsl.CheckIn; +import org.alfresco.cmis.dsl.CmisAssertion; +import org.alfresco.cmis.dsl.CmisUtil; +import org.alfresco.cmis.dsl.DocumentVersioning; +import org.alfresco.cmis.dsl.JmxUtil; +import org.alfresco.cmis.dsl.QueryExecutor; +import org.alfresco.utility.LogFactory; +import org.alfresco.utility.Utility; +import org.alfresco.utility.constants.UserRole; +import org.alfresco.utility.dsl.DSLContentModelAction; +import org.alfresco.utility.dsl.DSLFile; +import org.alfresco.utility.dsl.DSLFolder; +import org.alfresco.utility.dsl.DSLProtocol; +import org.alfresco.utility.exception.TestConfigurationException; +import org.alfresco.utility.model.ContentModel; +import org.alfresco.utility.model.DataListItemModel; +import org.alfresco.utility.model.DataListModel; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.GroupModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.UserModel; +import org.apache.chemistry.opencmis.client.api.CmisObject; +import org.apache.chemistry.opencmis.client.api.Document; +import org.apache.chemistry.opencmis.client.api.FileableCmisObject; +import org.apache.chemistry.opencmis.client.api.Folder; +import org.apache.chemistry.opencmis.client.api.ObjectId; +import org.apache.chemistry.opencmis.client.api.Repository; +import org.apache.chemistry.opencmis.client.api.SecondaryType; +import org.apache.chemistry.opencmis.client.api.Session; +import org.apache.chemistry.opencmis.client.api.SessionFactory; +import org.apache.chemistry.opencmis.client.runtime.SessionFactoryImpl; +import org.apache.chemistry.opencmis.commons.PropertyIds; +import org.apache.chemistry.opencmis.commons.SessionParameter; +import org.apache.chemistry.opencmis.commons.data.AclCapabilities; +import org.apache.chemistry.opencmis.commons.data.ContentStream; +import org.apache.chemistry.opencmis.commons.data.PermissionMapping; +import org.apache.chemistry.opencmis.commons.data.RepositoryInfo; +import org.apache.chemistry.opencmis.commons.enums.AclPropagation; +import org.apache.chemistry.opencmis.commons.enums.BaseTypeId; +import org.apache.chemistry.opencmis.commons.enums.BindingType; +import org.apache.chemistry.opencmis.commons.enums.UnfileObject; +import org.apache.chemistry.opencmis.commons.enums.VersioningState; +import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException; +import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException; +import org.apache.chemistry.opencmis.commons.exceptions.CmisStorageException; +import org.apache.chemistry.opencmis.commons.exceptions.CmisVersioningException; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import static org.alfresco.utility.Utility.checkObjectIsInitialized; +import static org.alfresco.utility.report.log.Step.STEP; + +@Service +@Scope(value = "prototype") +public class CmisWrapper extends DSLProtocol implements DSLContentModelAction, DSLFile, DSLFolder +{ + protected Logger LOG = LogFactory.getLogger(); + public static String STEP_PREFIX = "CMIS:"; + + private Session session; + + @Autowired + private AuthParameterProviderFactory authParameterProviderFactory; + + @Autowired + CmisProperties cmisProperties; + + public List deleteTreeFailedObjects = new ArrayList(); + + @Override + public CmisWrapper authenticateUser(UserModel userModel) + { + return authenticateUser(userModel, authParameterProviderFactory.getDefaultProvider()); + } + + public CmisWrapper authenticateUser(UserModel userModel, Function> authParameterProvider) + { + disconnect(); + STEP(String.format("%s Connect with %s/%s", STEP_PREFIX, userModel.getUsername(), userModel.getPassword())); + SessionFactory factory = SessionFactoryImpl.newInstance(); + + // Initialise a new session parameter map with session authentication parameters for userModel + Map parameter = new HashMap<>(authParameterProvider.apply(userModel)); + + String binding = cmisProperties.getCmisBinding().toLowerCase(); + String cmisURLPath = cmisProperties.envProperty().getFullServerUrl() + cmisProperties.getBasePath(); + if (binding.equals(BindingType.BROWSER.value())) + { + parameter.put(SessionParameter.BROWSER_URL, cmisURLPath); + parameter.put(SessionParameter.BINDING_TYPE, BindingType.BROWSER.value()); + LOG.info("Using binding type [{}] to [{}] and credentials: {}", BindingType.BROWSER.value(), cmisURLPath, userModel.toString()); + } + else if (binding.equals(BindingType.ATOMPUB.value().replace("pub", ""))) + { + parameter.put(SessionParameter.ATOMPUB_URL, cmisURLPath); + parameter.put(SessionParameter.BINDING_TYPE, BindingType.ATOMPUB.value()); + LOG.info("Using binding type [{}] to [{}] and credentials: {}", BindingType.ATOMPUB.value(), cmisURLPath, userModel.toString()); + } + else if (binding.equals(BindingType.WEBSERVICES.value())) + { + parameter.put(SessionParameter.WEBSERVICES_REPOSITORY_SERVICE, cmisURLPath); + parameter.put(SessionParameter.WEBSERVICES_NAVIGATION_SERVICE, cmisURLPath); + parameter.put(SessionParameter.WEBSERVICES_OBJECT_SERVICE, cmisURLPath); + parameter.put(SessionParameter.WEBSERVICES_VERSIONING_SERVICE, cmisURLPath); + parameter.put(SessionParameter.WEBSERVICES_DISCOVERY_SERVICE, cmisURLPath); + parameter.put(SessionParameter.WEBSERVICES_MULTIFILING_SERVICE, cmisURLPath); + parameter.put(SessionParameter.WEBSERVICES_RELATIONSHIP_SERVICE, cmisURLPath); + parameter.put(SessionParameter.WEBSERVICES_ACL_SERVICE, cmisURLPath); + parameter.put(SessionParameter.WEBSERVICES_POLICY_SERVICE, cmisURLPath); + parameter.put(SessionParameter.BINDING_TYPE, BindingType.WEBSERVICES.value()); + LOG.info("Using binding type [{}] to [{}] and credentials: {}", BindingType.WEBSERVICES.value(), cmisURLPath, userModel.toString()); + } + + parameter.put(SessionParameter.CONNECT_TIMEOUT, "20000"); + parameter.put(SessionParameter.READ_TIMEOUT, "60000"); + List repositories = factory.getRepositories(parameter); + parameter.put(SessionParameter.REPOSITORY_ID, repositories.get(0).getId()); + session = repositories.get(0).createSession(); + setTestUser(userModel); + return this; + } + + public CmisWrapper authUserUsingBrowserUrlAndBindingType(UserModel userModel, String urlPath, String bindingType) + { + STEP(String.format("%s Setting binding type %s to %s", STEP_PREFIX, bindingType, urlPath)); + STEP(String.format("%s Connect with %s/%s", STEP_PREFIX, userModel.getUsername(), userModel.getPassword())); + SessionFactory factory = SessionFactoryImpl.newInstance(); + // Initialise a new session parameter map with session authentication parameters for userModel + Map parameter = new HashMap<>(authParameterProviderFactory.getDefaultProvider().apply(userModel)); + + parameter.put(SessionParameter.BROWSER_URL, urlPath); + parameter.put(SessionParameter.BINDING_TYPE, bindingType); + LOG.info("Using binding type [{}] to [{}] and credentials: {}", bindingType, urlPath, userModel.toString()); + List repositories = factory.getRepositories(parameter); + parameter.put(SessionParameter.REPOSITORY_ID, repositories.get(0).getId()); + session = repositories.get(0).createSession(); + setTestUser(userModel); + return this; + } + + public AuthParameterProviderFactory getAuthParameterProviderFactory() + { + return this.authParameterProviderFactory; + } + + @Override + public CmisWrapper disconnect() + { + if (session != null) + { + getSession().clear(); + } + return this; + } + + @Override + public String buildPath(String parent, String... paths) + { + return Utility.convertBackslashToSlash(super.buildPath(parent, paths)).replace("//", "/"); + } + + /** + * Get the current session + * + * @return Session + */ + public synchronized Session getSession() + { + return session; + } + + @Override + public CmisWrapper createFile(FileModel fileModel) + { + return createFile(fileModel, BaseTypeId.CMIS_DOCUMENT.value(), VersioningState.MAJOR); + } + + /** + * Create a new file + * + * @param fileModel {@link FileModel} file model to be created + * @param versioningState {@link VersioningState} + * @return CmisWrapper + */ + public CmisWrapper createFile(FileModel fileModel, VersioningState versioningState) + { + return createFile(fileModel, BaseTypeId.CMIS_DOCUMENT.value(), versioningState); + } + + /** + * Create a new file + * + * @param fileModel {@link FileModel} file model to be created + * @param cmisBaseTypeId base type id (e.g. 'cmis:document') + * @param versioningState {@link VersioningState} + * @return CmisWrapper + */ + public CmisWrapper createFile(FileModel fileModel, String cmisBaseTypeId, VersioningState versioningState) + { + return createFile(fileModel, withCMISUtil().getProperties(fileModel, cmisBaseTypeId), versioningState); + } + + public CmisWrapper createFile(FileModel fileModel, Map properties, VersioningState versioningState) + { + ContentStream contentStream = dataContent.getContentStream(fileModel.getName(), fileModel.getContent()); + STEP(String.format("%s Create file '%s' in '%s'", STEP_PREFIX, fileModel.getName(), getCurrentSpace())); + Document doc = null; + try + { + doc = withCMISUtil().getCmisFolder(getCurrentSpace()).createDocument(properties, contentStream, versioningState); + } + catch (CmisStorageException se) + { + doc = withCMISUtil().getCmisFolder(getCurrentSpace()).createDocument(properties, contentStream, versioningState); + } + fileModel.setNodeRef(doc.getId()); + String location = buildPath(getCurrentSpace(), fileModel.getName()); + setLastResource(location); + fileModel.setProtocolLocation(location); + fileModel.setCmisLocation(location); + dataContent.closeContentStream(contentStream); + return this; + } + + /** + * Create new file from existing one (that was set in last resource) + * + * @param newfileModel {@link FileModel} file model to be created + * @param sourceFileModel {@link ContentModel} source file model + * @return CmisWrapper + */ + public CmisWrapper createFileFromSource(FileModel newfileModel, ContentModel sourceFileModel) + { + return createFileFromSource(newfileModel, sourceFileModel, BaseTypeId.CMIS_DOCUMENT.value()); + } + + /** + * Create new file from existing one with versioning state set to Major(that was set in last resource) + * + * @param newfileModel {@link FileModel} file model to be created + * @param sourceFileModel {@link ContentModel} source file model + * @param cmisBaseTypeId base type id (e.g. 'cmis:document') + * @return CmisWrapper + */ + public CmisWrapper createFileFromSource(FileModel newfileModel, ContentModel sourceFileModel, String cmisBaseTypeId) + { + return createFileFromSource(newfileModel, sourceFileModel, cmisBaseTypeId, VersioningState.MAJOR); + } + + /** + * Create new file from existing one (that was set in last resource) + * + * @param newfileModel {@link FileModel} file model to be created + * @param sourceFileModel {@link ContentModel} source file model + * @param versioningState version(e.g. 'VersioningState.MAJOR') + * @return CmisWrapper + */ + public CmisWrapper createFileFromSource(FileModel newfileModel, ContentModel sourceFileModel, VersioningState versioningState) + { + return createFileFromSource(newfileModel, sourceFileModel, BaseTypeId.CMIS_DOCUMENT.value(), versioningState); + } + + /** + * Create new file from existing one (that was set in last resource) + * + * @param newfileModel {@link FileModel} file model to be created + * @param sourceFileModel {@link ContentModel} source file model + * @param cmisBaseTypeId base type id (e.g. 'cmis:document') + * @param versioningState (e.g. 'VersioningState.MAJOR') + * @return CmisWrapper + */ + public CmisWrapper createFileFromSource(FileModel newfileModel, ContentModel sourceFileModel, String cmisBaseTypeId, VersioningState versioningState) + { + String resourcePath = getLastResource(); + STEP(String.format("%s Create new file '%s' from source '%s' in '%s'", STEP_PREFIX, newfileModel.getName(), sourceFileModel.getName(), resourcePath)); + Document source = withCMISUtil().getCmisDocument(sourceFileModel.getCmisLocation()); + Map properties = withCMISUtil().getProperties(newfileModel, cmisBaseTypeId); + Document doc = withCMISUtil().getCmisFolder(resourcePath).createDocumentFromSource(source, properties, versioningState); + doc.refresh(); + newfileModel.setNodeRef(doc.getId()); + String location = buildPath(resourcePath, doc.getName()); + setLastResource(location); + newfileModel.setProtocolLocation(location); + newfileModel.setCmisLocation(location); + return this; + } + + @Override + public CmisWrapper createFolder(FolderModel folderModel) + { + return createFolder(folderModel, BaseTypeId.CMIS_FOLDER.value()); + } + + public CmisWrapper createFolder(FolderModel folderModel, String cmisBaseTypeId) + { + Map properties = withCMISUtil().getProperties(folderModel, cmisBaseTypeId); + createFolder(folderModel, properties); + return this; + } + + public CmisWrapper createFolder(FolderModel folderModel, Map properties) + { + STEP(String.format("%s Create folder '%s' in '%s'", STEP_PREFIX, folderModel.getName(), getCurrentSpace())); + Folder folder = withCMISUtil().getCmisFolder(getCurrentSpace()).createFolder(properties); + String location = buildPath(getCurrentSpace(), folderModel.getName()); + setLastResource(location); + folderModel.setProtocolLocation(location); + folderModel.setCmisLocation(location); + folderModel.setNodeRef(folder.getId()); + return this; + } + + /** + * Deletes this folder and all subfolders with all versions and continue on failure + * + * @return current wrapper + */ + public CmisWrapper deleteFolderTree() + { + return deleteFolderTree(true, UnfileObject.DELETE, true); + } + + /** + * Deletes this folder and all subfolders with specific parameters + * + * @param allVersions + * @param unfile {@link UnfileObject} + * @param continueOnFailure + * @return current wrapper + */ + public CmisWrapper deleteFolderTree(boolean allVersions, UnfileObject unfile, boolean continueOnFailure) + { + String path = getLastResource(); + Folder parent = withCMISUtil().getCmisFolder(Utility.convertBackslashToSlash(new File(path).getParent())); + STEP(String.format("%s Delete parent folder from '%s'", STEP_PREFIX, path)); + Folder folder = withCMISUtil().getCmisFolder(path); + folder.refresh(); + deleteTreeFailedObjects.clear(); + deleteTreeFailedObjects = folder.deleteTree(allVersions, unfile, continueOnFailure); + for (String failedObj : deleteTreeFailedObjects) + { + LOG.error(String.format("Failed to delete object %s", failedObj)); + } + if (!deleteTreeFailedObjects.isEmpty()) + { + LOG.info(String.format("Retry: delete parent folder from %s", path)); + Utility.waitToLoopTime(2); + folder.refresh(); + folder.deleteTree(allVersions, unfile, continueOnFailure); + } + else + { + parent.refresh(); + dataContent.waitUntilContentIsDeleted(path); + } + return this; + } + + @Override + public String getRootPath() throws TestConfigurationException + { + return "/"; + } + + @Override + public String getSitesPath() throws TestConfigurationException + { + return String.format("%s/%s", getPrefixSpace(), "Sites"); + } + + @Override + public String getUserHomesPath() throws TestConfigurationException + { + return String.format("%s/%s", getPrefixSpace(), "User Homes"); + } + + @Override + public String getDataDictionaryPath() throws TestConfigurationException + { + return String.format("%s/%s", getPrefixSpace(), "Data Dictionary"); + } + + public String getSharedPath() throws TestConfigurationException + { + return String.format("%s/%s", getPrefixSpace(), "Shared"); + } + + @Override + public CmisWrapper usingSite(String siteId) + { + STEP(String.format("%s Navigate to site '%s/%s'", STEP_PREFIX, siteId, "documentLibrary")); + checkObjectIsInitialized(siteId, "SiteID"); + setCurrentSpace(buildSiteDocumentLibraryPath(siteId, "")); + return this; + } + + @Override + public CmisWrapper usingSite(SiteModel siteModel) + { + STEP(String.format("%s Navigate to site '%s/%s'", STEP_PREFIX, siteModel.getId(), "documentLibrary")); + checkObjectIsInitialized(siteModel, "SiteModel"); + String path = buildSiteDocumentLibraryPath(siteModel.getId(), ""); + setCurrentSpace(path); + return this; + } + + @Override + public CmisWrapper usingUserHome(String username) + { + STEP(String.format("%s Navigate to 'User Home' folder", STEP_PREFIX)); + checkObjectIsInitialized(username, "username"); + setCurrentSpace(buildUserHomePath(username, "")); + return this; + } + + @Override + public CmisWrapper usingUserHome() + { + STEP(String.format("%s Navigate to 'User Home' folder", STEP_PREFIX)); + checkObjectIsInitialized(getTestUser().getUsername(), "username"); + setCurrentSpace(buildUserHomePath(getTestUser().getUsername(), "")); + return this; + } + + public CmisWrapper usingShared() + { + STEP(String.format("%s Navigate to 'Shared' folder", STEP_PREFIX)); + setCurrentSpace(getSharedPath()); + return this; + } + + @Override + public CmisWrapper usingResource(ContentModel model) + { + STEP(String.format("%s Navigate to '%s'", STEP_PREFIX, model.getName())); + checkObjectIsInitialized(model, "contentName"); + setCurrentSpace(model.getCmisLocation()); + return this; + } + + @Override + protected String getProtocolJMXConfigurationStatus() + { + return ""; + } + + @Override + public String getPrefixSpace() + { + return ""; + } + + @Override + public CmisWrapper rename(String newName) + { + String resourcePath = getLastResource(); + CmisObject objToRename = withCMISUtil().getCmisObject(resourcePath); + STEP(String.format("%s Rename '%s' to '%s'", STEP_PREFIX, objToRename.getName(), newName)); + objToRename.rename(newName); + setLastResource(buildPath(new File(resourcePath).getParent(), newName)); + return this; + } + + @Override + public CmisWrapper update(String content) + { + return update(content, true); + } + + public CmisWrapper update(String content, boolean isLastChunk) + { + Document doc = withCMISUtil().getCmisDocument(getLastResource()); + doc.refresh(); + Utility.waitToLoopTime(2); + STEP(String.format("%s Update content from '%s' by appending '%s'", STEP_PREFIX, doc.getName(), content)); + ContentStream contentStream = dataContent.getContentStream(doc.getName(), content); + doc.appendContentStream(contentStream, isLastChunk); + dataContent.closeContentStream(contentStream); + return this; + } + + /** + * Update the properties of the last resource {@link ContentModel}. + * Example updateProperty("cmis:name", "test1234") + * + * @param property + * @param value + * @return + */ + public CmisWrapper updateProperty(String property, Object value) + { + String lastResource = getLastResource(); + CmisObject objSource = withCMISUtil().getCmisObject(lastResource); + STEP(String.format("%s Update '%s' property for '%s'", STEP_PREFIX, property, objSource.getName())); + Map properties = new HashMap<>(); + properties.put(property, value); + objSource.updateProperties(properties, true); + + if (property.equals("cmis:name")) + { + if (objSource instanceof Document) + { + setLastResource(buildPath(new File(getLastResource()).getParent(), objSource.getName())); + } + else if (objSource instanceof Folder) + { + setLastResource(buildPath(((Folder) objSource).getFolderParent().getPath(), value.toString())); + } + } + return this; + } + + @Override + public CmisWrapper delete() + { + String resourcePath = getLastResource(); + STEP(String.format("%s Delete content from '%s'", STEP_PREFIX, resourcePath)); + withCMISUtil().getCmisObject(resourcePath).delete(); + return this; + } + + /** + * Deletes all versions if parameter is set to true, otherwise deletes only last version + * + * @param allVersions + * @return + */ + public CmisWrapper deleteAllVersions(boolean allVersions) + { + String resourcePath = getLastResource(); + if (allVersions) + STEP(String.format("%s Delete all content '%s' versions", STEP_PREFIX, resourcePath)); + else + STEP(String.format("%s Delete only the last content '%s' version", STEP_PREFIX, resourcePath)); + withCMISUtil().getCmisObject(getLastResource()).delete(allVersions); + return this; + } + + /** + * Delete content stream + * + * @return + */ + public CmisWrapper deleteContent() + { + String resourcePath = getLastResource(); + STEP(String.format("%s Delete document content from '%s'", STEP_PREFIX, resourcePath)); + withCMISUtil().getCmisDocument(getLastResource()).deleteContentStream(); + return this; + } + + /** + * Delete content stream and refresh document + * + * @param refresh boolean refresh resource + * @return + */ + public CmisWrapper deleteContent(boolean refresh) + { + String resourcePath = getLastResource(); + STEP(String.format("%s Delete document content from '%s'", STEP_PREFIX, resourcePath)); + withCMISUtil().getCmisDocument(getLastResource()).deleteContentStream(refresh); + return this; + } + + /** + * Set the content stream for a document + * + * @param content String content to set + * @param overwrite + * @return + */ + public CmisWrapper setContent(String content, boolean overwrite) + { + Utility.waitToLoopTime(1); + Document doc = withCMISUtil().getCmisDocument(getLastResource()); + doc.refresh(); + STEP(String.format("%s Set '%s' content to '%s' - node: %s", STEP_PREFIX, content, doc.getName(), doc.getId())); + ContentStream contentStream = dataContent.getContentStream(doc.getName(), content); + try + { + doc.setContentStream(contentStream, overwrite, true); + } + catch (CmisStorageException cs) + { + doc.setContentStream(contentStream, overwrite, true); + } + dataContent.closeContentStream(contentStream); + return this; + } + + /** + * Set the content stream for a document with overwrite set to TRUE + * + * @param content + * @return + */ + public CmisWrapper setContent(String content) + { + return setContent(content, true); + } + + /** + * Create a 'R:cm:basis' relationship between a source document and a target document + * + * @param targetContent + * @return + */ + public CmisWrapper createRelationshipWith(ContentModel targetContent) + { + return createRelationshipWith(targetContent, "R:cm:basis"); + } + + /** + * Create relationship between a source document and a target document + * + * @param targetContent {@link ContentModel} + * @param relationType + * @return + */ + public CmisWrapper createRelationshipWith(ContentModel targetContent, String relationType) + { + STEP(String.format("%s Set %s relationship between source from '%s' and target '%s'", STEP_PREFIX, relationType, getLastResource(), + targetContent.getName())); + Map properties = new HashMap<>(); + properties.put(PropertyIds.OBJECT_TYPE_ID, relationType); + properties.put(PropertyIds.SOURCE_ID, withCMISUtil().getCmisObject(getLastResource()).getId()); + properties.put(PropertyIds.TARGET_ID, targetContent.getNodeRef()); + getSession().createRelationship(properties); + return this; + } + + /** + * Method allows you to file a document object in more than one folder. + * + * @param destination - the destination folder to which this document will be added + * @param allVersions - if this parameter is true, then all versions of the document will be added to the destination folder + * @return + */ + public CmisWrapper addDocumentToFolder(FolderModel destination, boolean allVersions) + { + CmisObject objSource = withCMISUtil().getCmisObject(getLastResource()); + Folder objDestination = withCMISUtil().getCmisFolder(destination.getCmisLocation()); + STEP(String.format("%s Add object '%s' to '%s'", STEP_PREFIX, objSource.getName(), destination.getCmisLocation())); + ((FileableCmisObject) objSource).addToFolder(objDestination, allVersions); + setLastResource(buildPath(destination.getCmisLocation(), objSource.getName())); + return this; + } + + /** + * Method allows you to remove a document object from the given folder. + * + * @param parentFolder - the folder from which this object should be removed + * @return + */ + public CmisWrapper removeDocumentFromFolder(FolderModel parentFolder) + { + CmisObject objSource = withCMISUtil().getCmisObject(getLastResource()); + Folder parentObj = withCMISUtil().getCmisFolder(parentFolder.getCmisLocation()); + STEP(String.format("%s Remove object '%s' from '%s'", STEP_PREFIX, objSource.getName(), parentFolder.getCmisLocation())); + ((FileableCmisObject) objSource).removeFromFolder(parentObj); + return this; + } + + /** + * Get child folders from a parent folder + * + * @return List + */ + @Override + public List getFolders() + { + STEP(String.format("%s Get the folder children from '%s'", STEP_PREFIX, getLastResource())); + return withCMISUtil().getFolders(); + } + + /** + * Get child documents from a parent folder + * + * @return List + */ + @Override + public List getFiles() + { + STEP(String.format("%s Get the file children from '%s'", STEP_PREFIX, getLastResource())); + return withCMISUtil().getFiles(); + } + + @Override + public CmisWrapper copyTo(ContentModel destination) + { + String source = getLastResource(); + String sourceName = new File(source).getName(); + STEP(String.format("%s Copy '%s' to '%s'", STEP_PREFIX, sourceName, destination.getCmisLocation())); + CmisObject objSource = withCMISUtil().getCmisObject(source); + + CmisObject objDestination = withCMISUtil().getCmisObject(destination.getCmisLocation()); + if (objSource instanceof Document) + { + Document d = (Document) objSource; + d.copy(objDestination); + } + else if (objSource instanceof Folder) + { + Folder fFrom = (Folder) objSource; + Folder toFolder = (Folder) objDestination; + withCMISUtil().copyFolder(fFrom, toFolder); + } + setLastResource(buildPath(destination.getCmisLocation(), sourceName)); + return this; + } + + @Override + public CmisWrapper moveTo(ContentModel destination) + { + String source = getLastResource(); + String sourceName = new File(source).getName(); + STEP(String.format("%s Move '%s' to '%s'", STEP_PREFIX, sourceName, destination.getCmisLocation())); + CmisObject objSource = withCMISUtil().getCmisObject(source); + CmisObject objDestination = withCMISUtil().getCmisObject(destination.getCmisLocation()); + if (objSource instanceof Document) + { + Document d = (Document) objSource; + List parents = d.getParents(); + CmisObject parent = getSession().getObject(parents.get(0).getId()); + d.move(parent, objDestination); + } + else if (objSource instanceof Folder) + { + Folder f = (Folder) objSource; + List parents = f.getParents(); + CmisObject parent = getSession().getObject(parents.get(0).getId()); + f.move(parent, objDestination); + } + setLastResource(buildPath(destination.getCmisLocation(), sourceName)); + return this; + } + + public RepositoryInfo getRepositoryInfo() + { + STEP(String.format("Get repository information for user %s", getCurrentUser().getUsername())); + return getSession().getRepositoryInfo(); + } + + public AclCapabilities getAclCapabilities() + { + return getRepositoryInfo().getAclCapabilities(); + } + + /** + * Checks out the document + */ + public CmisWrapper checkOut() + { + Document document = withCMISUtil().getCmisDocument(getLastResource()); + STEP(String.format("%s Check out document '%s'", STEP_PREFIX, document.getName())); + try + { + document.checkOut(); + } + catch (CmisRuntimeException e) + { + document.checkOut(); + } + return this; + } + + /** + * If this is a PWC (private working copy) the check out will be reversed. + */ + public CmisWrapper cancelCheckOut() + { + Document document = withCMISUtil().getCmisDocument(getLastResource()); + STEP(String.format("%s Cancel document '%s' check out", STEP_PREFIX, document.getName())); + document.cancelCheckOut(); + return this; + } + + /** + * Starts the process to check in a document + */ + public CheckIn prepareDocumentForCheckIn() + { + return new CheckIn(this); + } + + /** + * Reloads the resource from the repository + */ + public CmisWrapper refreshResource() + { + CmisObject cmisObject = withCMISUtil().getCmisObject(getLastResource()); + STEP(String.format("%s Reload '%s'", STEP_PREFIX, cmisObject.getName())); + cmisObject.refresh(); + return this; + } + + /** + * @return utilities that are used by CMIS + */ + public CmisUtil withCMISUtil() + { + return new CmisUtil(this); + } + + /** + * @return JMX DSL for this wrapper + */ + public JmxUtil withJMX() + { + return new JmxUtil(this, jmxBuilder.getJmxClient()); + } + + @Override + public CmisAssertion assertThat() + { + return new CmisAssertion(this); + } + + /** + * Starts the process to work with a version of a document + */ + public DocumentVersioning usingVersion() + { + return new DocumentVersioning(this, withCMISUtil().getCmisObject(getLastResource())); + } + + /** + * Add new permission for user + * + * @param user UserModel user + * @param role UserRole role to add + * @param aclPropagation AclPropagation propagation + * @return + */ + public CmisWrapper addAcl(UserModel user, UserRole role, AclPropagation aclPropagation) + { + STEP(String.format("%s Add permission '%s' for user %s ", STEP_PREFIX, role.name(), user.getUsername())); + withCMISUtil().getCmisObject(getLastResource(), withCMISUtil().setIncludeAclContext()).addAcl(withCMISUtil().createAce(user, role), aclPropagation); + return this; + } + + /** + * Add new permission for a group + * + * @param group GroupModel group + * @param role UserRole role to add + * @param aclPropagation AclPropagation propagation + * @return + */ + public CmisWrapper addAcl(GroupModel group, UserRole role, AclPropagation aclPropagation) + { + STEP(String.format("%s Add permission '%s' for user %s ", STEP_PREFIX, role.name(), group.getDisplayName())); + withCMISUtil().getCmisObject(getLastResource(), withCMISUtil().setIncludeAclContext()).addAcl(withCMISUtil().createAce(group, role), aclPropagation); + return this; + } + + /** + * Add new permission for a group + * + * @param group GroupModel group + * @param role UserRole role to add + * @return + */ + public CmisWrapper addAcl(GroupModel group, UserRole role) + { + return addAcl(group, role, null); + } + + /** + * Add new permissions to user + * + * @param user {@link UserModel} + * @param permissions to add ({@link PermissionMapping} can be used) + * @return + */ + public CmisWrapper addAcl(UserModel user, String... permissions) + { + withCMISUtil().getCmisObject(getLastResource(), withCMISUtil().setIncludeAclContext()).addAcl(withCMISUtil().createAce(user, permissions), null); + return this; + } + + /** + * Add new permission for user + * + * @param user UserModel user + * @param role UserRole role to add + * @return + */ + public CmisWrapper addAcl(UserModel user, UserRole role) + { + return addAcl(user, role, null); + } + + /** + * Update permission for user. + * If the role to remove is invalid a {@link CmisConstraintException} is thrown. + * + * @param user UserModel user + * @param newRole UserRole new role to add + * @param removeRole UserRole remove already added role + * @param aclPropagation AclPropagation + * @return + */ + public CmisWrapper applyAcl(UserModel user, UserRole newRole, UserRole removeRole, AclPropagation aclPropagation) + { + STEP(String.format("%s Edit permission for user %s from %s to %s ", STEP_PREFIX, user.getUsername(), removeRole.name(), newRole.name())); + withCMISUtil().getCmisObject(getLastResource(), withCMISUtil().setIncludeAclContext()).applyAcl(withCMISUtil().createAce(user, newRole), + withCMISUtil().createAce(user, removeRole), aclPropagation); + return this; + } + + /** + * Update permission for user. + * If the role to remove is invalid a {@link CmisConstraintException} is thrown. + * + * @param user UserModel user + * @param newRole UserRole new role to add + * @param removeRole UserRole remove already added role + * @return + */ + public CmisWrapper applyAcl(UserModel user, UserRole newRole, UserRole removeRole) + { + return applyAcl(user, newRole, removeRole, null); + } + + /** + * Update permission for user. + * If the permission to remove is invalid a {@link CmisConstraintException} is thrown. + * + * @param user {@link UserModel } + * @param newPermission permissions to add ({@link PermissionMapping} can be used) + * @param removePermission permissions to remove ({@link PermissionMapping} can be used) + * @return + */ + public CmisWrapper applyAcl(UserModel user, String newPermission, String removePermission) + { + STEP(String.format("%s Edit permission for user %s from %s to %s ", STEP_PREFIX, user.getUsername(), removePermission, newPermission)); + withCMISUtil().getCmisObject(getLastResource(), withCMISUtil().setIncludeAclContext()).applyAcl(withCMISUtil().createAce(user, newPermission), + withCMISUtil().createAce(user, removePermission), null); + return this; + } + + /** + * Remove permission from user + * + * @param user UserModel user + * @param removeRole UserRole role to remove + * @param aclPropagation AclPropagation + * @return + */ + public CmisWrapper removeAcl(UserModel user, UserRole removeRole, AclPropagation aclPropagation) + { + STEP(String.format("%s Remove permission '%s' from user %s ", STEP_PREFIX, removeRole.name(), user.getUsername())); + withCMISUtil().getCmisObject(getLastResource(), withCMISUtil().setIncludeAclContext()).removeAcl(withCMISUtil().createAce(user, removeRole), + aclPropagation); + return this; + } + + /** + * Remove permission from user + * + * @param user UserModel user + * @param removeRole UserRole role to remove + * @return + */ + public CmisWrapper removeAcl(UserModel user, UserRole removeRole) + { + return removeAcl(user, removeRole, null); + } + + public CmisWrapper removeAcl(UserModel user, String permissionToRemove) + { + STEP(String.format("%s Remove permission '%s' from user %s ", STEP_PREFIX, permissionToRemove, user.getUsername())); + withCMISUtil().getCmisObject(getLastResource(), withCMISUtil().setIncludeAclContext()).removeAcl(withCMISUtil().createAce(user, permissionToRemove), + null); + return this; + } + + /** + * Pass a string CMIS query, that will be handled by {@link QueryExecutor} using {@link org.apache.chemistry.opencmis.client.api.Session#query(String, boolean)} + * + * @param query + * @return {@link QueryExecutor} will all DSL assertions on returned restult + */ + public QueryExecutor withQuery(String query) + { + return new QueryExecutor(this, query); + } + + /** + * Use this method if the document is checked out. If not {@link CmisVersioningException} will be thrown. + * + * @return + */ + public CmisWrapper usingPWCDocument() + { + STEP(String.format("%s Navigate to private working copy of content '%s'", STEP_PREFIX, withCMISUtil().getPWCFileModel().getName())); + setCurrentSpace(withCMISUtil().getPWCFileModel().getCmisLocation()); + return this; + } + + /** + * @param baseType + * @return the DSL of asserting BaseObject type children for example. + */ + public BaseObjectType usingObjectType(String baseType) + { + return new BaseObjectType(this, baseType); + } + + /** + * Create a new data list type + * + * @param dataListModel {@link DataListModel} + * @return + */ + public CmisWrapper createDataList(DataListModel dataListModel) + { + Map properties = withCMISUtil().getProperties(dataListModel, "F:dl:dataList"); + properties.put("dl:dataListItemType", dataListModel.getDataListItemType()); + Folder folder = withCMISUtil().getCmisFolder(getCurrentSpace()).createFolder(properties); + String location = buildPath(getCurrentSpace(), dataListModel.getName()); + setLastResource(location); + dataListModel.setProtocolLocation(location); + dataListModel.setCmisLocation(location); + dataListModel.setNodeRef(folder.getId()); + return this; + } + + /** + * Create new data list item + * + * @param itemModel {@link DataListItemModel} + * @return + */ + public CmisWrapper createDataListItem(DataListItemModel itemModel) + { + Map propertyMap = itemModel.getItemProperties(); + String name = (String) propertyMap.get(PropertyIds.NAME); + STEP(String.format("%s Create new data list item %s (type: %s)", STEP_PREFIX, name, propertyMap.get(PropertyIds.OBJECT_TYPE_ID))); + ObjectId itemId = getSession().createDocument(propertyMap, withCMISUtil().getCmisObject(getLastResource()), null, null); + String path = buildPath(getCurrentSpace(), name); + itemModel.setName(name); + itemModel.setCmisLocation(path); + itemModel.setProtocolLocation(path); + itemModel.setNodeRef(itemId.getId()); + setLastResource(path); + return this; + } + + /** + * Attach documents to existent item set in last resource + * + * @param documents {@link ContentModel} list of content to attach + * @return + */ + public CmisWrapper attachDocument(ContentModel... contents) + { + String itemId = withCMISUtil().getCmisObject(getLastResource()).getId(); + for (ContentModel content : contents) + { + STEP(String.format("Attach document %s to item %s", content.getName(), itemId)); + Map relProps = new HashMap(); + relProps.put(PropertyIds.OBJECT_TYPE_ID, "R:cm:attachments"); + relProps.put(PropertyIds.SOURCE_ID, itemId); + relProps.put(PropertyIds.TARGET_ID, content.getNodeRef()); + getSession().createRelationship(relProps); + } + return this; + } + + /** + * Assign user to existent item set in last resource + * + * @param user {@link UserModel} + * @param relationType e.g. R:dl:issueAssignedTo, R:dl:assignee, R:dl:taskAssignee + * @return + */ + public CmisWrapper assignToUser(UserModel user, String relationType) + { + Map relProps = new HashMap(); + relProps.put(PropertyIds.OBJECT_TYPE_ID, relationType); + relProps.put(PropertyIds.SOURCE_ID, withCMISUtil().getCmisObject(getLastResource()).getId()); + relProps.put(PropertyIds.TARGET_ID, withCMISUtil().getUserNodeRef(user)); + getSession().createRelationship(relProps); + return this; + } + + /** + * Add new secondary types + * + * @param secondaryTypes e.g. P:cm:effectivity, P:audio:audio, P:cm:dublincore + * @return + */ + public CmisWrapper addSecondaryTypes(String...secondaryTypes) + { + CmisObject object = withCMISUtil().getCmisObject(getLastResource()); + List secondaryTypesNew = new ArrayList(); + for(SecondaryType oldType : object.getSecondaryTypes()) + { + secondaryTypesNew.add(oldType.getId()); + } + for(String newType : secondaryTypes) + { + secondaryTypesNew.add(newType); + } + Map properties = new HashMap(); + properties.put(PropertyIds.SECONDARY_OBJECT_TYPE_IDS, secondaryTypesNew); + object.updateProperties(properties); + return this; + } +} diff --git a/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/BaseObjectType.java b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/BaseObjectType.java new file mode 100644 index 0000000000..b5f1787f09 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/BaseObjectType.java @@ -0,0 +1,185 @@ +package org.alfresco.cmis.dsl; + +import static org.alfresco.utility.report.log.Step.STEP; + +import java.util.Iterator; +import java.util.List; + +import org.alfresco.cmis.CmisWrapper; +import org.alfresco.utility.LogFactory; +import org.apache.chemistry.opencmis.client.api.ItemIterable; +import org.apache.chemistry.opencmis.client.api.ObjectType; +import org.apache.chemistry.opencmis.client.api.Tree; +import org.apache.chemistry.opencmis.client.runtime.objecttype.ObjectTypeHelper; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.slf4j.Logger; +import org.testng.Assert; + +/** + * DSL for preparing calls on getting the type children of a type. + */ +public class BaseObjectType +{ + private CmisWrapper cmisAPI; + private String baseTypeID; + private boolean includePropertyDefinition = false; + private Logger LOG = LogFactory.getLogger(); + + public BaseObjectType(CmisWrapper cmisAPI, String baseTypeID) + { + this.cmisAPI = cmisAPI; + this.baseTypeID = baseTypeID; + } + + public BaseObjectType withPropertyDefinitions() + { + this.includePropertyDefinition = true; + return this; + } + + public BaseObjectType withoutPropertyDefinitions() + { + this.includePropertyDefinition = false; + return this; + } + + /** + * Example of objectTypeID: + * "D:trx:transferReport" - see {@link ObjectTypeHelper} "D:trx:tempTransferStore" + * "D:imap:imapAttach" + * + * @param objectTypeID + */ + public PropertyDefinitionObject hasChildren(String objectTypeID) + { + return checkChildren(objectTypeID, true); + } + + /** + * Example of objectTypeID: + * "D:trx:transferReport" - see {@link ObjectTypeHelper} "D:trx:tempTransferStore" + * "D:imap:imapAttach" + * + * @param objectTypeID + */ + public CmisWrapper doesNotHaveChildren(String objectTypeID) + { + checkChildren(objectTypeID, false); + return cmisAPI; + } + + /** + * Example of objectTypeID: + * "D:trx:transferReport" - see {@link ObjectTypeHelper} "D:trx:tempTransferStore" + * "D:imap:imapAttach" + * + * @param objectTypeID + */ + private PropertyDefinitionObject checkChildren(String objectTypeID, boolean exist) + { + ItemIterable values = cmisAPI.withCMISUtil().getTypeChildren(this.baseTypeID, includePropertyDefinition); + boolean foundChild = false; + PropertyDefinitionObject propDefinition = null; + for (Iterator iterator = values.iterator(); iterator.hasNext();) + { + ObjectType type = (ObjectType) iterator.next(); + LOG.info("Found child Object Type: {}", ToStringBuilder.reflectionToString(type, ToStringStyle.MULTI_LINE_STYLE)); + if (type.getId().equals(objectTypeID)) + { + foundChild = true; + propDefinition = new PropertyDefinitionObject(type); + break; + } + } + Assert.assertEquals(foundChild, exist, + String.format("Object Type with ID[%s] is found as children for Parent Type: [%s]", objectTypeID, this.baseTypeID)); + return propDefinition; + } + + public class PropertyDefinitionObject + { + ObjectType type; + + public PropertyDefinitionObject(ObjectType type) + { + this.type = type; + } + + public PropertyDefinitionObject propertyDefinitionIsEmpty() + { + STEP(String.format("%s Verify that property definitions map is empty.", CmisWrapper.STEP_PREFIX)); + Assert.assertTrue(type.getPropertyDefinitions().isEmpty(), "Property definitions is empty."); + return this; + } + + public PropertyDefinitionObject propertyDefinitionIsNotEmpty() + { + STEP(String.format("%s Verify that property definitions map is not empty.", CmisWrapper.STEP_PREFIX)); + Assert.assertFalse(type.getPropertyDefinitions().isEmpty(), "Property definitions is not empty."); + return this; + } + } + + private CmisWrapper checkDescendents(int depth, boolean exist, String... objectTypeIDs) + { + List> values = cmisAPI.withCMISUtil().getTypeDescendants(this.baseTypeID, depth, includePropertyDefinition); + for (String objectTypeID : objectTypeIDs) + { + boolean foundChild = false; + for (Tree tree : values) + { + if (tree.getItem().getId().equals(objectTypeID)) + { + foundChild = true; + break; + } + } + Assert.assertEquals(foundChild, exist, + String.format("Assert %b: Descendant [%s] is found as descendant for Type: [%s]", exist, objectTypeID, this.baseTypeID)); + + if (foundChild) + { + STEP(String.format("%s Cmis object '%s' is found as descendant.", CmisWrapper.STEP_PREFIX, objectTypeID)); + } + else + { + STEP(String.format("%s Cmis object '%s' is NOT found as descendant.", CmisWrapper.STEP_PREFIX, objectTypeID)); + } + } + return cmisAPI; + } + + /** + * Assert that specified descendantType is present in the depth of tree + * Depth can be -1 or >= 1 + * Example of objectTypeID: + * "D:trx:transferReport" - see {@link ObjectTypeHelper} "D:trx:tempTransferStore" + * "D:imap:imapAttach" + * + * @param objectTypeID + * @param depth + * @return + */ + public CmisWrapper hasDescendantType(int depth, String... objectTypeIDs) + { + return checkDescendents(depth, true, objectTypeIDs); + } + + /** + * Assert that specified descendantType is NOT present in the depth of tree + * Depth can be -1 or >= 1 + * Example of objectTypeID: + * "D:trx:transferReport" - see {@link ObjectTypeHelper} "D:trx:tempTransferStore" + * "D:imap:imapAttach" + * + * @param objectTypeID + * @param depth + * @return + */ + public CmisWrapper doesNotHaveDescendantType(int depth, String... objectTypeIDs) + { + checkDescendents(depth, false, objectTypeIDs); + return cmisAPI; + } +} diff --git a/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/CheckIn.java b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/CheckIn.java new file mode 100644 index 0000000000..1dbcc3f3dc --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/CheckIn.java @@ -0,0 +1,78 @@ +package org.alfresco.cmis.dsl; + +import org.alfresco.cmis.CmisWrapper; +import org.alfresco.utility.Utility; +import org.apache.chemistry.opencmis.client.api.Document; +import org.apache.chemistry.opencmis.commons.data.ContentStream; +import org.apache.chemistry.opencmis.commons.exceptions.CmisStorageException; + +import java.util.Map; + +/** + * DSL pertaining only to check in a {@link Document} + */ +public class CheckIn +{ + private CmisWrapper cmisWrapper; + private boolean version; + private Map properties; + private String content; + private String comment; + + public CheckIn(CmisWrapper cmisWrapper) + { + this.cmisWrapper = cmisWrapper; + } + + public CheckIn withMajorVersion() + { + this.version = true; + return this; + } + + public CheckIn withMinorVersion() + { + this.version = false; + return this; + } + + public CheckIn withContent(String content) + { + this.content = content; + return this; + } + + public CheckIn withoutComment() + { + this.comment = null; + return this; + } + + public CheckIn withComment(String comment) + { + this.comment = comment; + return this; + } + + public CmisWrapper checkIn() throws Exception + { + return checkIn(properties); + } + + public CmisWrapper checkIn(Map properties) throws Exception + { + ContentStream contentStream = cmisWrapper.withCMISUtil().getContentStream(content); + try + { + Document pwc = cmisWrapper.withCMISUtil().getPWCDocument(); + pwc.refresh(); + Utility.waitToLoopTime(2); + pwc.checkIn(version, properties, contentStream, comment); + } + catch(CmisStorageException st) + { + cmisWrapper.withCMISUtil().getPWCDocument().checkIn(version, properties, contentStream, comment); + } + return cmisWrapper; + } +} diff --git a/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/CmisAssertion.java b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/CmisAssertion.java new file mode 100644 index 0000000000..df612d6891 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/CmisAssertion.java @@ -0,0 +1,1167 @@ +package org.alfresco.cmis.dsl; + +import static org.alfresco.utility.report.log.Step.STEP; + +import java.io.File; +import java.util.ArrayList; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.cmis.CmisWrapper; +import org.alfresco.utility.constants.UserRole; +import org.alfresco.utility.dsl.DSLAssertion; +import org.alfresco.utility.exception.TestConfigurationException; +import org.alfresco.utility.model.ContentModel; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.GroupModel; +import org.alfresco.utility.model.UserModel; +import org.apache.chemistry.opencmis.client.api.ChangeEvent; +import org.apache.chemistry.opencmis.client.api.CmisObject; +import org.apache.chemistry.opencmis.client.api.Document; +import org.apache.chemistry.opencmis.client.api.Folder; +import org.apache.chemistry.opencmis.client.api.ItemIterable; +import org.apache.chemistry.opencmis.client.api.ObjectType; +import org.apache.chemistry.opencmis.client.api.OperationContext; +import org.apache.chemistry.opencmis.client.api.Property; +import org.apache.chemistry.opencmis.client.api.Relationship; +import org.apache.chemistry.opencmis.client.api.Rendition; +import org.apache.chemistry.opencmis.client.api.SecondaryType; +import org.apache.chemistry.opencmis.client.api.Session; +import org.apache.chemistry.opencmis.client.runtime.OperationContextImpl; +import org.apache.chemistry.opencmis.commons.data.Ace; +import org.apache.chemistry.opencmis.commons.data.Acl; +import org.apache.chemistry.opencmis.commons.data.CmisExtensionElement; +import org.apache.chemistry.opencmis.commons.enums.Action; +import org.apache.chemistry.opencmis.commons.enums.BindingType; +import org.apache.chemistry.opencmis.commons.enums.ChangeType; +import org.apache.chemistry.opencmis.commons.enums.ExtensionLevel; +import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships; +import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException; +import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException; +import org.apache.commons.lang3.StringUtils; +import org.testng.Assert; + +/** + * DSL with all assertion available for {@link CmisWrapper} + */ +public class CmisAssertion extends DSLAssertion +{ + public static String STEP_PREFIX = "CMIS:"; + + public CmisAssertion(CmisWrapper cmisAPI) + { + super(cmisAPI); + } + + public CmisWrapper cmisAPI() + { + return getProtocol(); + } + + @Override + public CmisWrapper existsInRepo() + { + STEP(String.format("CMIS: Assert that content '%s' exists in repository", cmisAPI().getLastResource())); + Assert.assertTrue(!cmisAPI().withCMISUtil().getCmisObject(cmisAPI().getLastResource()).getId().isEmpty(), + String.format("Content {%s} was found in repository", cmisAPI().getLastResource())); + return cmisAPI(); + } + + @Override + public CmisWrapper doesNotExistInRepo() + { + STEP(String.format("CMIS: Assert that content '%s' does not exist in repository", cmisAPI().getLastResource())); + boolean notFound = false; + try + { + cmisAPI().getSession().clear(); + cmisAPI().withCMISUtil().getCmisObject(cmisAPI().getLastResource()); + } + catch (CmisObjectNotFoundException | CmisRuntimeException e) + { + notFound = true; + } + Assert.assertTrue(notFound, String.format("Content {%s} was NOT found in repository", cmisAPI().getLastResource())); + return cmisAPI(); + } + + /** + * Verify changes for a specific object from cmis log + * + * @param model {@link ContentModel} + * @param changeTypes {@link ChangeType} + * @return + * @throws Exception + */ + public CmisWrapper contentModelHasChanges(ContentModel model, ChangeType... changeTypes) throws Exception + { + String token = cmisAPI().getRepositoryInfo().getLatestChangeLogToken(); + if (StringUtils.isEmpty(token)) + { + throw new TestConfigurationException("Please enable CMIS audit"); + } + ItemIterable events = cmisAPI().getSession().getContentChanges(token, true); + String lastObjectId = model.getNodeRef(); + boolean isChange = false; + for (ChangeType changeType : changeTypes) + { + STEP(String.format("%s Verify action %s for content: %s", CmisWrapper.STEP_PREFIX, changeType, model.getName())); + isChange = false; + for (ChangeEvent event : events) + { + if (event.getObjectId().equals(lastObjectId)) + { + if (changeType == event.getChangeType()) + { + isChange = true; + break; + } + } + } + Assert.assertTrue(isChange, String.format("Action %s for content: '%s' was found", changeType, model.getName())); + } + return cmisAPI(); + } + + /** + * Verify that a specific object does not have changes from cmis log + * + * @param model {@link ContentModel} + * @param changeTypes {@link ChangeType} + * @return + * @throws Exception + */ + public CmisWrapper contentModelDoesnotHaveChangesWithWrongToken(ContentModel model, ChangeType... changeTypes) throws Exception + { + String token = cmisAPI().getRepositoryInfo().getLatestChangeLogToken(); + if (StringUtils.isEmpty(token)) + { + throw new TestConfigurationException("Please enable CMIS audit"); + } + ItemIterable events = cmisAPI().getSession().getContentChanges(token + 1, true); + String lastObjectId = model.getNodeRef(); + boolean isChange = false; + for (ChangeType changeType : changeTypes) + { + STEP(String.format("%s Verify action %s for content: %s", CmisWrapper.STEP_PREFIX, changeType, model.getName())); + isChange = false; + for (ChangeEvent event : events) + { + if (event.getObjectId().equals(lastObjectId)) + { + if (changeType == event.getChangeType()) + { + isChange = true; + break; + } + } + } + Assert.assertFalse(isChange, String.format("Action %s for content: '%s' was found", changeType, model.getName())); + } + return cmisAPI(); + } + + /** + * Check if the {@link #getLastResource()} has the list of {@link Action} Example: + * {code} + * .hasAllowableActions(Action.CAN_CREATE_FOLDER); + * {code} + * + * @param actions + * @return + */ + public CmisWrapper hasAllowableActions(Action... actions) + { + CmisObject cmisObject = cmisAPI().withCMISUtil().getCmisObject(cmisAPI().getLastResource()); + for (Action action : actions) + { + STEP(String.format("%s Verify if object %s has allowable action %s", CmisWrapper.STEP_PREFIX, cmisObject.getName(), action.name())); + Assert.assertTrue(cmisObject.hasAllowableAction(action), String.format("Object %s does not have action %s", cmisObject.getName(), action.name())); + } + return cmisAPI(); + } + + /** + * Check if {@link #getLastResource()} object has actions returned + * from {@link org.apache.chemistry.opencmis.client.api.CmisObject#getAllowableActions()} + * + * @param actions + * @return + */ + public CmisWrapper isAllowableActionInList(Action... actions) + { + List currentActions = cmisAPI().withCMISUtil().getAllowableActions(); + for (Action action : actions) + { + STEP(String.format("%s Verify that action '%s' exists", CmisWrapper.STEP_PREFIX, action.name())); + Assert.assertTrue(currentActions.contains(action), String.format("Action %s was found", action.name())); + } + return cmisAPI(); + } + + /** + * Check if the {@link #getLastResource()) does not have the list of {@link Action} Example: + * {code} + * .doesNotHaveAllowableActions(Action.CAN_CREATE_FOLDER); + * {code} + * + * @param actions + * @return + */ + public CmisWrapper doesNotHaveAllowableActions(Action... actions) + { + CmisObject cmisObject = cmisAPI().withCMISUtil().getCmisObject(cmisAPI().getLastResource()); + for (Action action : actions) + { + STEP(String.format("%s Verify if object %s does not have allowable action %s", CmisWrapper.STEP_PREFIX, cmisObject.getName(), action.name())); + Assert.assertFalse(cmisObject.hasAllowableAction(action), String.format("Object %s does not have action %s", cmisObject.getName(), action.name())); + } + return cmisAPI(); + } + + /** + * Verify document content + * + * @param content String expected content + * @return + * @throws Exception + */ + public CmisWrapper contentIs(String content) throws Exception + { + STEP(String.format("%s Verify if content '%s' is the expected one", CmisWrapper.STEP_PREFIX, content)); + Assert.assertEquals(cmisAPI().withCMISUtil().getDocumentContent(), content, + String.format("The content of file %s - is the expected one", cmisAPI().getLastResource())); + return cmisAPI(); + } + + /** + * Verify document content contains specific details + * + * @param content String expected content + * @return + * @throws Exception + */ + public CmisWrapper contentContains(String content) throws Exception + { + STEP(String.format("%s Verify if content '%s' is the expected one", CmisWrapper.STEP_PREFIX, content)); + Assert.assertTrue(cmisAPI().withCMISUtil().getDocumentContent().contains(content), + String.format("The content of file %s - is the expected one", cmisAPI().getLastResource())); + return cmisAPI(); + } + + /** + * Verify if current resource has the id given + * + * @param id - expected object id + * @return + */ + public CmisWrapper objectIdIs(String id) + { + CmisObject objSource = cmisAPI().withCMISUtil().getCmisObject(cmisAPI().getLastResource()); + STEP(String.format("%s Verify if '%s' object has '%s' id", CmisWrapper.STEP_PREFIX, objSource.getName(), id)); + Assert.assertEquals(objSource.getId(), id, "Object has id."); + return cmisAPI(); + } + + /** + * Verify the value of the given property + * + * @param property - the property id + * @param value - expected property value + * @return + */ + public CmisWrapper contentPropertyHasValue(String property, String value) + { + CmisObject objSource = cmisAPI().withCMISUtil().getCmisObject(cmisAPI().getLastResource()); + STEP(String.format("%s Verify if '%s' property for '%s' content has '%s' value", CmisWrapper.STEP_PREFIX, property, objSource.getName(), value)); + Object propertyValue = objSource.getPropertyValue(property); + if (propertyValue instanceof ArrayList) + { + @SuppressWarnings({ "unchecked", "rawtypes" }) + ArrayList values = (ArrayList) propertyValue; + Assert.assertEquals(values.get(0).toString(), value, "Property has value."); + } + else + { + Assert.assertEquals(propertyValue.toString(), value, "Property has value."); + } + return cmisAPI(); + } + + /** + * Verify if {@link Document} is checked out + * + * @return + */ + public CmisWrapper documentIsCheckedOut() + { + Document document = cmisAPI().withCMISUtil().getCmisDocument(cmisAPI().getLastResource()); + STEP(String.format("%s Verify if document '%s' is checked out", CmisWrapper.STEP_PREFIX, document.getName())); + Assert.assertTrue(document.isVersionSeriesCheckedOut(), "Document is checkedout"); + return cmisAPI(); + } + + /** + * Verify if {@link Document} is private working copy (pwc) + * + * @return + */ + public CmisWrapper isPrivateWorkingCopy() + { + Document document = cmisAPI().withCMISUtil().getCmisDocument(cmisAPI().getLastResource()); + STEP(String.format("%s Verify if document '%s' is private working copy", CmisWrapper.STEP_PREFIX, document.getName())); + + // Alfresco supports BindingType.WEBSERVICES for CMIS 1.0 + // (BindingType.ATOMPUB and BindingType.BROWSER for CMIS 1.1) + // and "cmis:isPrivateWorkingCopy" was introduced with CMIS 1.1. + // + // Checking if the document is a pwc through + // https://chemistry.apache.org/java/javadoc/org/apache/chemistry/opencmis/client/api/DocumentProperties.html#isPrivateWorkingCopy-- + // won't work for BindingType.WEBSERVICES + // + // Thus using + // https://chemistry.apache.org/java/javadoc/org/apache/chemistry/opencmis/client/api/Document.html#isVersionSeriesPrivateWorkingCopy-- + // which is supported in all CMIS versions. + Assert.assertTrue(cmisAPI().withCMISUtil().isPrivateWorkingCopy()); + + return cmisAPI(); + } + + /** + * Verify that {@link Document} is not private working copy (pwc) + * + * @return + */ + public CmisWrapper isNotPrivateWorkingCopy() + { + Document document = cmisAPI().withCMISUtil().getCmisDocument(cmisAPI().getLastResource()); + STEP(String.format("%s Verify if document '%s' PWC is not private working copy", CmisWrapper.STEP_PREFIX, document.getName())); + Assert.assertFalse(cmisAPI().withCMISUtil().isPrivateWorkingCopy()); + return cmisAPI(); + } + + /** + * Verify that {@link Document} is not checked out + * + * @return + */ + public CmisWrapper documentIsNotCheckedOut() + { + Document document = cmisAPI().withCMISUtil().getCmisDocument(cmisAPI().getLastResource()); + STEP(String.format("%s Verify if document '%s' is not checked out", CmisWrapper.STEP_PREFIX, document.getName())); + Assert.assertFalse(document.isVersionSeriesCheckedOut(), "Document is not checked out"); + return cmisAPI(); + } + + /** + * Verify if there is a relationship between current resource and the given target + * + * @param targetContent + * @return + */ + public CmisWrapper objectHasRelationshipWith(ContentModel targetContent) + { + OperationContext oc = new OperationContextImpl(); + oc.setIncludeRelationships(IncludeRelationships.SOURCE); + + CmisObject source = cmisAPI().withCMISUtil().getCmisObject(cmisAPI().getLastResource(), oc); + CmisObject target = cmisAPI().withCMISUtil().getCmisObject(targetContent.getCmisLocation()); + + STEP(String.format("%s Verify if source '%s' has relationship with '%s'", CmisWrapper.STEP_PREFIX, source.getName(), target.getName())); + List relTargetIds = new ArrayList<>(); + for (Relationship rel : source.getRelationships()) + { + relTargetIds.add(rel.getTarget().getId()); + } + Assert.assertTrue(relTargetIds.contains(target.getId()), + String.format("Relationship is created between source '%s' and target '%s'.", source.getName(), target.getName())); + return cmisAPI(); + } + + /** + * Verify document has version + * + * @param version String expected version + * @return + * @throws Exception + */ + public CmisWrapper documentHasVersion(double version) throws Exception + { + Document document = cmisAPI().withCMISUtil().getCmisDocument(cmisAPI().getLastResource()); + document.refresh(); + STEP(String.format("%s Verify if document '%s' has version '%s'", CmisWrapper.STEP_PREFIX, document.getName(), version)); + Assert.assertEquals(Double.parseDouble(document.getVersionLabel()), version, "File has version"); + return cmisAPI(); + } + + /** + * Verify parent from the {@link Folder} set as last resource + * + * @param contentModel + * @return + */ + public CmisWrapper folderHasParent(ContentModel contentModel) + { + STEP(String.format("%s Verify folder %s has parent %s", CmisWrapper.STEP_PREFIX, cmisAPI().getLastResource(), contentModel.getName())); + Assert.assertEquals(cmisAPI().withCMISUtil().getFolderParent().getName(), contentModel.getName(), "Folder name is not the expected one"); + return cmisAPI(); + } + + /** + * Verify base type id + * + * @param baseTypeId String expected object type value + * @return + * @throws Exception + */ + public CmisWrapper baseTypeIdIs(String baseTypeId) throws Exception + { + STEP(String.format("%s Verify if base object type '%s' is the expected one", CmisWrapper.STEP_PREFIX, baseTypeId)); + String actualBaseTypeIdValue = cmisAPI().withCMISUtil().getCmisObject(cmisAPI().getLastResource()).getType().getBaseTypeId().value(); + Assert.assertEquals(actualBaseTypeIdValue, baseTypeId, "Object type is the expected one"); + return cmisAPI(); + } + + /** + * Verify object type id + * + * @param objectTypeId String expected object type value + * @return + * @throws Exception + */ + public CmisWrapper objectTypeIdIs(String objectTypeId) throws Exception + { + STEP(String.format("%s Verify if object type id '%s' is the expected one", CmisWrapper.STEP_PREFIX, objectTypeId)); + String typeId = cmisAPI().withCMISUtil().getCmisObject(cmisAPI().getLastResource()).getType().getId(); + if (StringUtils.isEmpty(typeId)) + { + typeId = ""; + } + Assert.assertEquals(cmisAPI().withCMISUtil().getCmisObject(cmisAPI().getLastResource()).getType().getId(), objectTypeId, + "Object type id is the expected one"); + return cmisAPI(); + } + + /** + * Verify a specific object property + * + * @param propertyId + * @param value + * @return + */ + public CmisWrapper objectHasProperty(String propertyId, Object value) + { + CmisObject cmisObject = cmisAPI().withCMISUtil().getCmisObject(cmisAPI().getLastResource()); + STEP(String.format("%s Verify if object %s has property %s ", CmisWrapper.STEP_PREFIX, cmisObject.getName(), propertyId)); + Property property = cmisAPI().withCMISUtil().getProperty(propertyId); + Object propValue = property.getValue(); + if(propValue instanceof GregorianCalendar) + { + Date date = (Date) value; + long longDate = date.getTime(); + long actualDate = ((GregorianCalendar) propValue).getTimeInMillis(); + Assert.assertEquals(actualDate, longDate); + } + else + { + if (propValue == null) + { + propValue = ""; + } + Assert.assertEquals(property.getValue().toString(), value.toString(), String.format("Found property value %s", value)); + } + return cmisAPI(); + } + + /** + * Check if CMIS object contains a property. + * Example: + * ...assertObjectHasProperty("cmis:secondaryObjectTypeIds", "Secondary Object Type Ids","secondaryObjectTypeIds", "cmis:secondaryObjectTypeIds", + * "P:cm:titled", "P:sys:localized"); + * + * @param propertyId + * @param displayName + * @param localName + * @param queryName + * @param values + * @return + */ + public CmisWrapper objectHasProperty(String propertyId, String displayName, String localName, String queryName, String... values) + { + CmisObject cmisObject = cmisAPI().withCMISUtil().getCmisObject(cmisAPI().getLastResource()); + STEP(String.format("%s Verify if object %s has property %s ", CmisWrapper.STEP_PREFIX, cmisObject.getName(), propertyId)); + Property property = cmisAPI().withCMISUtil().getProperty(propertyId); + if (property != null) + { + Assert.assertEquals(property.getDisplayName(), displayName, "Property displayName"); + Assert.assertEquals(property.getLocalName(), localName, "Property localName"); + Assert.assertEquals(property.getQueryName(), queryName, "Property queryName"); + for (String value : values) + { + Assert.assertTrue(property.getValues().contains(value), "Property value"); + } + } + else + { + throw new AssertionError(String.format("Object %s does not have property %s", cmisObject.getName(), propertyId)); + } + return cmisAPI(); + } + + /** + * Assert if the {@link #getLastResource()) has the latest major version set + */ + public CmisWrapper isLatestMajorVersion() + { + String path = cmisAPI().getLastResource(); + STEP(String.format("%s Verify that document from '%s' is latest major version", CmisWrapper.STEP_PREFIX, path)); + Assert.assertTrue(cmisAPI().withCMISUtil().getCmisDocument(path).isLatestMajorVersion(), String.format("Document from %s is last major version", path)); + return cmisAPI(); + } + + /** + * Verify that {@link Document} is not latest major version. + * + * @return + */ + public CmisWrapper isNotLatestMajorVersion() + { + String path = cmisAPI().getLastResource(); + STEP(String.format("%s Verify that document from '%s' is not latest major version", CmisWrapper.STEP_PREFIX, path)); + Assert.assertFalse(cmisAPI().withCMISUtil().getCmisDocument(path).isLatestMajorVersion(), + String.format("Document from %s is last major version", path)); + return cmisAPI(); + } + + /** + * Verify that renditions are available + */ + public CmisWrapper renditionIsAvailable() + { + STEP(String.format("%s Verify if renditions are available for %s", CmisWrapper.STEP_PREFIX, cmisAPI().getLastResource())); + List renditions = cmisAPI().withCMISUtil().getRenditions(); + Assert.assertTrue(renditions != null && !renditions.isEmpty()); + return cmisAPI(); + } + + /** + * Verify that thumbnail rendition is available + * + * @return + */ + public CmisWrapper thumbnailRenditionIsAvailable() + { + boolean found = false; + STEP(String.format("%s Verify if thumbnail rendition is available for %s", CmisWrapper.STEP_PREFIX, cmisAPI().getLastResource())); + List renditions = cmisAPI().withCMISUtil().getRenditions(); + for (Rendition rendition : renditions) + { + if (rendition.getKind().equals("cmis:thumbnail")) + { + found = true; + } + } + Assert.assertTrue(found, String.format("Thumbnail rendition found for", cmisAPI().getLastResource())); + return cmisAPI(); + } + + private boolean isSecondaryTypeAvailable(String secondaryTypeId) + { + boolean found = false; + List secondaryTypes = cmisAPI().withCMISUtil().getSecondaryTypes(); + for (SecondaryType type : secondaryTypes) + { + if (type.getId().equals(secondaryTypeId)) + { + found = true; + break; + } + } + + return found; + } + + /** + * Verify secondary type for specific {@link CmisObject} + * + * @param secondaryTypeId + * @return + */ + public CmisWrapper secondaryTypeIsAvailable(String secondaryTypeId) + { + STEP(String.format("%s Verify if '%s' secondary type is available for '%s'", CmisWrapper.STEP_PREFIX, secondaryTypeId, + new File(cmisAPI().getLastResource()).getName())); + Assert.assertTrue(isSecondaryTypeAvailable(secondaryTypeId), String.format("%s is available for %s", secondaryTypeId, cmisAPI().getLastResource())); + return cmisAPI(); + } + + /** + * Verify secondary type is not available for specific {@link CmisObject} + * + * @param secondaryTypeId + * @return + */ + public CmisWrapper secondaryTypeIsNotAvailable(String secondaryTypeId) + { + STEP(String.format("%s Verify if '%s' aspect is NOT available for %s", CmisWrapper.STEP_PREFIX, secondaryTypeId, cmisAPI().getLastResource())); + Assert.assertFalse(isSecondaryTypeAvailable(secondaryTypeId), + String.format("%s is NOT available for %s", secondaryTypeId, cmisAPI().getLastResource())); + return cmisAPI(); + } + + /** + * Verify document content length + * + * @param contentLength String expected content length + * @return + * @throws Exception + */ + public CmisWrapper contentLengthIs(long contentLength) throws Exception + { + STEP(String.format("%s Verify if content length '%s' is the expected one", CmisWrapper.STEP_PREFIX, contentLength)); + Document lastVersion = cmisAPI().withCMISUtil().getCmisDocument(cmisAPI().getLastResource()); + lastVersion.refresh(); + Assert.assertEquals(lastVersion.getContentStreamLength(), contentLength, "File content is the expected one"); + return cmisAPI(); + } + + /** + * e.g. assertFolderHasDescendant(1, file1) will verify if file1 is a direct descendant of {@link #getLastResource()} + * + * @param depth {@link #getFolderDescendants(int)} + * @param contentModels {@link #getCmisObjectsFromContentModels(ContentModel...)} + */ + public void hasDescendants(int depth, ContentModel... contentModels) + { + STEP(String.format("%s Assert that folder %s has descendants in depth %d:", STEP_PREFIX, getProtocol().getLastResource(), depth)); + CmisObject currentCmisObject = getProtocol().withCMISUtil().getCmisObject(getProtocol().getLastResource()); + List cmisObjects = getProtocol().withCMISUtil().getCmisObjectsFromContentModels(contentModels); + List folderDescendants = getProtocol().withCMISUtil().getFolderDescendants(depth); + for (CmisObject cmisObject : cmisObjects) + { + boolean found = false; + STEP(String.format("%s Verify that folder '%s' has descendant %s", CmisWrapper.STEP_PREFIX, currentCmisObject.getName(), cmisObject.getName())); + for (CmisObject folderDescendant : folderDescendants) + if (folderDescendant.getId().equals(cmisObject.getId())) + { + found = true; + break; + } + Assert.assertTrue(found, String.format("Folder %s does not have descendant %s", currentCmisObject.getName(), cmisObject)); + } + } + + public void doesNotHaveDescendants(int depth) + { + STEP(String.format("%s Assert that folder %s does not have descendants in depth %d:", STEP_PREFIX, getProtocol().getLastResource(), depth)); + CmisObject currentCmisObject = getProtocol().withCMISUtil().getCmisObject(getProtocol().getLastResource()); + List folderDescendants = getProtocol().withCMISUtil().getFolderDescendants(depth); + Assert.assertTrue(folderDescendants.isEmpty(), String.format("Folder %s should not have descendants", currentCmisObject.getName())); + } + + /** + * Verify that {@link CmisObject} has ACLs (Access Control Lists) + * + * @return + */ + public CmisWrapper hasAcls() + { + STEP(String.format("%s Verify that %s has acls", CmisWrapper.STEP_PREFIX, cmisAPI().getLastResource())); + String path = cmisAPI().getLastResource(); + STEP(String.format("%s Get Acls for %s", CmisWrapper.STEP_PREFIX, path)); + Assert.assertNotNull(cmisAPI().withCMISUtil().getAcls(), String.format("Acls found for %s", path)); + return cmisAPI(); + } + + /** + * Depending on the specified depth, checks that all the contents from contentModels list are present in the current folder tree structure + * + * @param depth the depth of the tree to check, must be -1 or >= 1 + * @param contentModels expected list of contents to be found in the tree + * @return + */ + public CmisWrapper hasFolderTree(int depth, ContentModel... contentModels) + { + CmisObject currentCmisObject = getProtocol().withCMISUtil().getCmisObject(getProtocol().getLastResource()); + List cmisObjects = getProtocol().withCMISUtil().getCmisObjectsFromContentModels(contentModels); + List folderDescendants = getProtocol().withCMISUtil().getFolderTree(depth); + for (CmisObject cmisObject : cmisObjects) + { + boolean found = false; + STEP(String.format("%s Verify that folder '%s' has folder tree %s", CmisWrapper.STEP_PREFIX, currentCmisObject.getName(), cmisObject.getName())); + for (CmisObject folderDescendant : folderDescendants) + if (folderDescendant.getId().equals(cmisObject.getId())) + found = true; + Assert.assertTrue(found, String.format("Folder %s does not have folder tree %s", currentCmisObject.getName(), cmisObject)); + } + return cmisAPI(); + } + + /** + * Verify the permission for a specific user from the last resource object + * + * @param userModel {@link UserModel} user to verify + * @param role {@link UserRole} user role to verify + * @return + */ + public CmisWrapper permissionIsSetForUser(UserModel userModel, UserRole role) + { + STEP(String.format("%s Verify that user %s has role %s set to content %s", CmisWrapper.STEP_PREFIX, userModel.getUsername(), role.name(), + cmisAPI().getLastResource())); + Assert.assertTrue(checkPermission(userModel.getUsername(), role.getRoleId()), + String.format("User %s has permission %s", userModel.getUsername(), role.name())); + return cmisAPI(); + } + + /** + * Verify the permission for a specific group of users from the last resource object + * + * @param groupModel {@link GroupModel} group to verify + * @param role {@link UserRole} user role to verify + * @return + */ + public CmisWrapper permissionIsSetForGrup(GroupModel groupModel, UserRole role) + { + STEP(String.format("%s Verify that user %s has role %s set to content %s", CmisWrapper.STEP_PREFIX, groupModel.getDisplayName(), role.name(), + cmisAPI().getLastResource())); + Assert.assertTrue(checkPermission(groupModel.getDisplayName(), role.getRoleId()), + String.format("User %s has permission %s", groupModel.getDisplayName(), role.name())); + return cmisAPI(); + } + + private boolean checkPermission(String user, String permission) + { + Acl acl = cmisAPI().withCMISUtil().getAcls(); + if (acl == null) + { + throw new CmisRuntimeException(String.format("No acls returned for '%s'", cmisAPI().getLastResource())); + } + List aces = acl.getAces(); + boolean found = false; + for (Ace ace : aces) + { + if (ace.getPrincipalId().equals(user) && ace.getPermissions().get(0).equals(permission)) + { + found = true; + break; + } + } + return found; + } + + /** + * Verify the permission for a specific user from the last resource object + * + * @param userModel {@link UserModel} + * @param permission to verify + * @return + */ + public CmisWrapper permissionIsSetForUser(UserModel userModel, String permission) + { + STEP(String.format("%s Verify that user %s has role %s set to content %s", CmisWrapper.STEP_PREFIX, userModel.getUsername(), permission, + cmisAPI().getLastResource())); + Assert.assertTrue(checkPermission(userModel.getUsername(), permission), + String.format("User %s has permission %s", userModel.getUsername(), permission)); + return cmisAPI(); + } + + /** + * Verify that permission is not set for a specific user from the last resource object + * + * @param userModel {@link UserModel} user to verify + * @param role {@link UserRole} user role to verify + * @return + */ + public CmisWrapper permissionIsNotSetForUser(UserModel userModel, UserRole role) + { + STEP(String.format("%s Verify that user %s doesn't have role %s set to content %s", CmisWrapper.STEP_PREFIX, userModel.getUsername(), role.name(), + cmisAPI().getLastResource())); + Assert.assertFalse(checkPermission(userModel.getUsername(), role.getRoleId()), + String.format("User %s has permission %s", userModel.getUsername(), role.name())); + return cmisAPI(); + } + + /** + * Verify that permission is not set for a specific user from the last resource object + * + * @param userModel {@link UserModel} user to verify + * @param permission to verify + * @return + */ + public CmisWrapper permissionIsNotSetForUser(UserModel userModel, String permission) + { + STEP(String.format("%s Verify that user %s doesn't have permission %s set to content %s", CmisWrapper.STEP_PREFIX, userModel.getUsername(), permission, + cmisAPI().getLastResource())); + Assert.assertFalse(checkPermission(userModel.getUsername(), permission), + String.format("User %s has permission %s", userModel.getUsername(), permission)); + return cmisAPI(); + } + + public CmisWrapper typeDefinitionIs(ContentModel contentModel) + { + CmisObject cmisObject = cmisAPI().withCMISUtil().getCmisObject(contentModel.getCmisLocation()); + STEP(String.format("%s Verify that object '%s' type definition matches '%s' type definition", CmisWrapper.STEP_PREFIX, cmisObject.getName(), + cmisAPI().withCMISUtil().getTypeDefinition().getId())); + Assert.assertTrue(cmisAPI().withCMISUtil().getTypeDefinition().equals(cmisObject.getType()), String.format( + "Object '%s' type definition does not match '%s' type definition", cmisObject.getName(), cmisAPI().withCMISUtil().getTypeDefinition().getId())); + return cmisAPI(); + } + + /** + * Verify that a specific folder(set by calling {@link org.alfresco.cmis.CmisWrapper#usingResource(ContentModel)}) + * contains checked out documents + * + * @param contentModels checked out documents to verify + * @return + */ + public CmisWrapper folderHasCheckedOutDocument(ContentModel... contentModels) + { + List cmisObjectList = cmisAPI().withCMISUtil().getCmisObjectsFromContentModels(contentModels); + List cmisCheckedOutDocuments = cmisAPI().withCMISUtil().getCheckedOutDocumentsFromFolder(); + for (CmisObject cmisObject : cmisObjectList) + { + Assert.assertTrue(cmisAPI().withCMISUtil().isCmisObjectContainedInCmisCheckedOutDocumentsList(cmisObject, cmisCheckedOutDocuments), + String.format("Folder %s does not contain checked out document %s", cmisAPI().getLastResource(), cmisObject)); + } + return cmisAPI(); + } + + /** + * Verify that a specific folder(set by calling {@link org.alfresco.cmis.CmisWrapper#usingResource(ContentModel)}) + * contains checked out documents in a specific order. + * + * @param context {@link OperationContext} + * @param contentModels documents to verify in the order returned by the {@link OperationContext} + * @return + */ + public CmisWrapper folderHasCheckedOutDocument(OperationContext context, ContentModel... contentModels) + { + List cmisObjectList = cmisAPI().withCMISUtil().getCmisObjectsFromContentModels(contentModels); + List cmisCheckedOutDocuments = cmisAPI().withCMISUtil().getCheckedOutDocumentsFromFolder(context); + for (int i = 0; i < cmisObjectList.size(); i++) + { + Assert.assertEquals(cmisObjectList.get(i).getId().split(";")[0], cmisCheckedOutDocuments.get(i).getId().split(";")[0], + String.format("Folder %s does not contain checked out document %s", cmisAPI().getLastResource(), cmisObjectList.get(i).getName())); + } + return cmisAPI(); + } + + /** + * Verify checked out documents from {@link Session} + * + * @param contentModels documents to verify + * @return + */ + public CmisWrapper sessionHasCheckedOutDocument(ContentModel... contentModels) + { + List cmisObjectList = cmisAPI().withCMISUtil().getCmisObjectsFromContentModels(contentModels); + List cmisCheckedOutDocuments = cmisAPI().withCMISUtil().getCheckedOutDocumentsFromSession(); + for (CmisObject cmisObject : cmisObjectList) + { + Assert.assertTrue(cmisAPI().withCMISUtil().isCmisObjectContainedInCmisCheckedOutDocumentsList(cmisObject, cmisCheckedOutDocuments), + String.format("Session does not contain checked out document %s", cmisObject)); + } + return cmisAPI(); + } + + /** + * Verify checked out documents from {@link Session} in a specific order set in {@link OperationContext} + * + * @param context {@link OperationContext} + * @param contentModels documents to verify + * @return + */ + public CmisWrapper sessionHasCheckedOutDocument(OperationContext context, ContentModel... contentModels) + { + List cmisObjectList = cmisAPI().withCMISUtil().getCmisObjectsFromContentModels(contentModels); + List cmisCheckedOutDocuments = cmisAPI().withCMISUtil().getCheckedOutDocumentsFromSession(context); + for (int i = 0; i < cmisObjectList.size(); i++) + { + Assert.assertEquals(cmisObjectList.get(i).getId().split(";")[0], cmisCheckedOutDocuments.get(i).getId().split(";")[0], + String.format("Session does not contain checked out document %s", cmisObjectList.get(i).getName())); + } + return cmisAPI(); + } + + /** + * Verify that checked out documents are not found in {@link Session} + * + * @param contentModels documents to verify + * @return + */ + public CmisWrapper sessioDoesNotHaveCheckedOutDocument(ContentModel... contentModels) + { + List cmisObjectList = cmisAPI().withCMISUtil().getCmisObjectsFromContentModels(contentModels); + List cmisCheckedOutDocuments = cmisAPI().withCMISUtil().getCheckedOutDocumentsFromSession(); + for (CmisObject cmisObject : cmisObjectList) + { + Assert.assertFalse(cmisAPI().withCMISUtil().isCmisObjectContainedInCmisCheckedOutDocumentsList(cmisObject, cmisCheckedOutDocuments), + String.format("Session does contain checked out document %s", cmisObject)); + } + return cmisAPI(); + } + + /** + * Verify that {@link CmisObject} has a specific aspect extension + * + * @param aspectId + * @return + */ + public CmisWrapper hasAspectExtension(String aspectId) + { + STEP(String.format("Verify that aspect %s is applied to %s", aspectId, cmisAPI().getLastResource())); + boolean found = false; + List extensions = cmisAPI().withCMISUtil().getCmisObject(cmisAPI().getLastResource()).getExtensions(ExtensionLevel.PROPERTIES); + for (CmisExtensionElement extElement : extensions) + { + if (extElement.getName().equals("aspects")) + { + List aspects = extElement.getChildren(); + for (CmisExtensionElement aspect : aspects) + { + if (aspect.getValue() != null) + { + if (aspect.getValue().equals(aspectId)) + { + found = true; + } + } + } + } + } + Assert.assertTrue(found, String.format("Aspect extension %s for %s was found", aspectId, cmisAPI().getLastResource())); + return cmisAPI(); + } + + /** + * Verify the parents for a {@link CmisObject} + * + * @param parentsList + * @return + */ + public CmisWrapper hasParents(String... parentsList) + { + List folderNames = new ArrayList<>(); + List parents = new ArrayList<>(); + String source = cmisAPI().getLastResource(); + CmisObject objSource = cmisAPI().withCMISUtil().getCmisObject(source); + STEP(String.format("%s Verify the parents for '%s'.", CmisWrapper.STEP_PREFIX, objSource.getName())); + if (objSource instanceof Document) + { + Document d = (Document) objSource; + parents = d.getParents(); + } + else if (objSource instanceof Folder) + { + Folder f = (Folder) objSource; + parents = f.getParents(); + } + for (Folder folder : parents) + { + folderNames.add(folder.getName()); + } + Assert.assertEqualsNoOrder(folderNames.toArray(), parentsList, "Parents list is the expected one."); + return cmisAPI(); + } + + public CmisWrapper descriptionIs(String description) + { + String source = cmisAPI().getLastResource(); + STEP(String.format("%s Verify object '%s' description is '%s'", CmisWrapper.STEP_PREFIX, source, description)); + CmisObject cmisObject = cmisAPI().withCMISUtil().getCmisObject(source); + Assert.assertEquals(description, cmisObject.getDescription()); + return cmisAPI(); + } + + /** + * Verify if folder children exist in parent folder + * + * @param fileModel children files + * @return + * @throws Exception + */ + public CmisWrapper hasFolders(FolderModel... folderModel) throws Exception + { + String currentSpace = cmisAPI().getCurrentSpace(); + List folders = cmisAPI().getFolders(); + for (FolderModel folder : folderModel) + { + STEP(String.format("%s Verify that folder %s is in %s", CmisWrapper.STEP_PREFIX, folder.getName(), currentSpace)); + Assert.assertTrue(cmisAPI().withCMISUtil().isFolderInList(folder, folders), String.format("Folder %s is in %s", folder.getName(), currentSpace)); + } + return cmisAPI(); + } + + /** + * Verify if file children exist in parent folder + * + * @param fileModel children files + * @return + * @throws Exception + */ + public CmisWrapper hasFiles(FileModel... fileModel) throws Exception + { + String currentSpace = cmisAPI().getLastResource(); + List files = cmisAPI().getFiles(); + for (FileModel file : fileModel) + { + STEP(String.format("%s Verify that file '%s' is in '%s'", CmisWrapper.STEP_PREFIX, file.getName(), currentSpace)); + Assert.assertTrue(cmisAPI().withCMISUtil().isFileInList(file, files), String.format("File %s is in %s", file.getName(), currentSpace)); + } + return cmisAPI(); + } + + /** + * Verify if file(s) children exist in parent folder + * + * @param fileModels children files + * @return + * @throws Exception + */ + public CmisWrapper doesNotHaveFile(FileModel... fileModels) throws Exception + { + String currentSpace = cmisAPI().getLastResource(); + List files = cmisAPI().getFiles(); + for (FileModel fileModel : fileModels) + { + STEP(String.format("%s Verify that file '%s' is not in '%s'", CmisWrapper.STEP_PREFIX, fileModel.getName(), currentSpace)); + Assert.assertFalse(cmisAPI().withCMISUtil().isFileInList(fileModel, files), String.format("File %s is in %s", fileModel.getName(), currentSpace)); + } + return cmisAPI(); + } + + /** + * Verify if folder(s) children exist in parent folder + * + * @param folderModels children files + * @return + * @throws Exception + */ + public CmisWrapper doesNotHaveFolder(FolderModel... folderModels) throws Exception + { + String currentSpace = cmisAPI().getLastResource(); + List folders = cmisAPI().getFolders(); + for (FolderModel folderModel : folderModels) + { + STEP(String.format("%s Verify that folder '%s' is not in '%s'", CmisWrapper.STEP_PREFIX, folderModel.getName(), currentSpace)); + Assert.assertFalse(cmisAPI().withCMISUtil().isFolderInList(folderModel, folders), + String.format("File %s is in %s", folderModel.getName(), currentSpace)); + } + return cmisAPI(); + } + + /** + * Verify the children(files and folders) from a parent folder + * + * @param contentModel children + * @return + * @throws Exception + */ + public CmisWrapper hasChildren(ContentModel... contentModel) throws Exception + { + String currentSpace = cmisAPI().getCurrentSpace(); + Map mapContents = cmisAPI().withCMISUtil().getChildren(); + List contents = new ArrayList(); + for (Map.Entry entry : mapContents.entrySet()) + { + contents.add(entry.getKey()); + } + for (ContentModel content : contentModel) + { + STEP(String.format("%s Verify that file %s is in %s", CmisWrapper.STEP_PREFIX, content.getName(), currentSpace)); + Assert.assertTrue(cmisAPI().withCMISUtil().isContentInList(content, contents), + String.format("Content %s is in %s", content.getName(), currentSpace)); + } + return cmisAPI(); + } + + + public CmisWrapper hasUniqueChildren(int numberOfChildren) throws Exception + { + STEP(String.format("%s Verify that current folder has %d unique children", CmisWrapper.STEP_PREFIX, numberOfChildren)); + Map mapContents = cmisAPI().withCMISUtil().getChildren(); + + Set documentIds = new HashSet(); + for (ContentModel key : mapContents.keySet()) + { + documentIds.add(key.getName()); + } + Assert.assertTrue(numberOfChildren==documentIds.size(), String.format("Current folder contains %d unique children, but expected is %d", documentIds.size(), numberOfChildren)); + return cmisAPI(); + } + + /** + * Get check in comment for last document version + * + * @param comment to verify + * @return + */ + public CmisWrapper hasCheckInCommentLastVersion(String comment) + { + String source = cmisAPI().getLastResource(); + STEP(String.format("%s Verify check in comment for last version of %s", CmisWrapper.STEP_PREFIX, source)); + Document document = cmisAPI().withCMISUtil().getCmisDocument(source); + Assert.assertEquals(comment, document.getCheckinComment(), String.format("Document %s has check in comment %s", document.getName(), comment)); + return cmisAPI(); + } + + /** + * Get check in comment for a specific document version + * + * @param documentVersion version of document + * @param comment to verify + * @return + */ + public CmisWrapper hasCheckInCommentForVersion(double documentVersion, String comment) + { + String source = cmisAPI().getLastResource(); + STEP(String.format("%s Verify check in comment for version %s of %s", CmisWrapper.STEP_PREFIX, documentVersion, source)); + String documentId = cmisAPI().withCMISUtil().getObjectId(source).split(";")[0]; + documentId = documentId + ";" + documentVersion; + Document document = (Document) cmisAPI().withCMISUtil().getCmisObjectById(documentId); + Assert.assertEquals(comment, document.getCheckinComment(), String.format("Document %s has check in comment %s", document.getName(), comment)); + return cmisAPI(); + } + + /** + * Verify failed deleted objects after delete tree action + * + * @param nodeRef objects to verify + * @return + */ + public CmisWrapper hasFailedDeletedObject(String nodeRef) + { + STEP(String.format("%s Verify failed deleted object from %s", CmisWrapper.STEP_PREFIX, cmisAPI().getLastResource())); + Assert.assertTrue(cmisAPI().deleteTreeFailedObjects.contains(nodeRef), String.format("Object %s found after delete", nodeRef)); + return cmisAPI(); + } + + /** + * Verify if there is a relationship between current resource and the given target + * + * @param user + * @return + */ + public CmisWrapper userIsAssigned(UserModel user) + { + OperationContext oc = new OperationContextImpl(); + oc.setIncludeRelationships(IncludeRelationships.SOURCE); + CmisObject source = cmisAPI().withCMISUtil().getCmisObject(cmisAPI().getLastResource(), oc); + String userNodeRef = cmisAPI().withCMISUtil().getUserNodeRef(user); + + STEP(String.format("%s Verify if user '%s' has relationship with '%s'", CmisWrapper.STEP_PREFIX, user.getUsername(), source.getName())); + List relTargetIds = new ArrayList<>(); + for (Relationship rel : source.getRelationships()) + { + relTargetIds.add(rel.getTarget().getId()); + } + Assert.assertTrue(relTargetIds.contains(userNodeRef), + String.format("Relationship is created between source '%s' and target '%s'.", source.getName(), user.getUsername())); + return cmisAPI(); + } +} diff --git a/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/CmisUtil.java b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/CmisUtil.java new file mode 100644 index 0000000000..07f51113a7 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/CmisUtil.java @@ -0,0 +1,761 @@ +package org.alfresco.cmis.dsl; + +import org.alfresco.cmis.CmisWrapper; +import org.alfresco.cmis.exception.InvalidCmisObjectException; +import org.alfresco.utility.LogFactory; +import org.alfresco.utility.Utility; +import org.alfresco.utility.constants.UserRole; +import org.alfresco.utility.exception.IORuntimeException; +import org.alfresco.utility.model.ContentModel; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.GroupModel; +import org.alfresco.utility.model.UserModel; +import org.apache.chemistry.opencmis.client.api.CmisObject; +import org.apache.chemistry.opencmis.client.api.Document; +import org.apache.chemistry.opencmis.client.api.FileableCmisObject; +import org.apache.chemistry.opencmis.client.api.Folder; +import org.apache.chemistry.opencmis.client.api.ItemIterable; +import org.apache.chemistry.opencmis.client.api.ObjectType; +import org.apache.chemistry.opencmis.client.api.OperationContext; +import org.apache.chemistry.opencmis.client.api.Property; +import org.apache.chemistry.opencmis.client.api.QueryResult; +import org.apache.chemistry.opencmis.client.api.Rendition; +import org.apache.chemistry.opencmis.client.api.SecondaryType; +import org.apache.chemistry.opencmis.client.api.Tree; +import org.apache.chemistry.opencmis.commons.PropertyIds; +import org.apache.chemistry.opencmis.commons.data.Ace; +import org.apache.chemistry.opencmis.commons.data.Acl; +import org.apache.chemistry.opencmis.commons.data.AclCapabilities; +import org.apache.chemistry.opencmis.commons.data.ContentStream; +import org.apache.chemistry.opencmis.commons.data.PermissionMapping; +import org.apache.chemistry.opencmis.commons.data.PropertyData; +import org.apache.chemistry.opencmis.commons.data.RepositoryInfo; +import org.apache.chemistry.opencmis.commons.enums.Action; +import org.apache.chemistry.opencmis.commons.enums.BaseTypeId; +import org.apache.chemistry.opencmis.commons.enums.BindingType; +import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException; +import org.apache.chemistry.opencmis.commons.exceptions.CmisVersioningException; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.testng.collections.Lists; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.alfresco.utility.report.log.Step.STEP; + +/** + * DSL utility for managing CMIS objects + */ +public class CmisUtil +{ + private CmisWrapper cmisAPI; + private Logger LOG = LogFactory.getLogger(); + + public CmisUtil(CmisWrapper cmisAPI) + { + this.cmisAPI = cmisAPI; + } + + /** + * Get cmis object by object id + * + * @param objectId cmis object id + * @return CmisObject cmis object + */ + public CmisObject getCmisObjectById(String objectId) + { + LOG.debug("Get CMIS object by ID {}", objectId); + if (cmisAPI.getSession() == null) + { + throw new CmisRuntimeException("Please authenticate user, call: cmisAPI.authenticate(..)!"); + } + if (objectId == null) + { + throw new InvalidCmisObjectException("Invalid content id"); + } + return cmisAPI.getSession().getObject(objectId); + } + + /** + * Get cmis object by object id with OperationContext + * + * @param objectId cmis object id + * @param context OperationContext + * @return CmisObject cmis object + */ + public CmisObject getCmisObjectById(String objectId, OperationContext context) + { + if (cmisAPI.getSession() == null) + { + throw new CmisRuntimeException("Please authenticate user, call: cmisAPI.authenticate(..)!"); + } + if (objectId == null) + { + throw new InvalidCmisObjectException("Invalid content id"); + } + return cmisAPI.getSession().getObject(objectId, context); + } + + /** + * Get cmis object by path + * + * @param pathToItem String path to item + * @return CmisObject cmis object + */ + public CmisObject getCmisObject(String pathToItem) + { + if (cmisAPI.getSession() == null) + { + throw new CmisRuntimeException("Please authenticate user, call: cmisAPI.authenticate(..)!"); + } + if (pathToItem == null) + { + throw new InvalidCmisObjectException("Invalid path set for content"); + } + CmisObject cmisObject = cmisAPI.getSession().getObjectByPath(Utility.removeLastSlash(pathToItem)); + if (cmisObject instanceof Document) + { + if (!((Document) cmisObject).getVersionLabel().contentEquals("pwc")) + { + // get last version of document + cmisObject = ((Document) cmisObject).getObjectOfLatestVersion(false); + } + else + { + // get pwc document + cmisObject = cmisAPI.getSession().getObject(((Document) cmisObject).getObjectOfLatestVersion(false).getVersionSeriesCheckedOutId()); + } + } + return cmisObject; + } + + /** + * Get cmis object by path with context + * + * @param pathToItem String path to item + * @param context OperationContext + * @return CmisObject cmis object + */ + public CmisObject getCmisObject(String pathToItem, OperationContext context) + { + if (cmisAPI.getSession() == null) + { + throw new CmisRuntimeException("Please authenticate user!"); + } + if (pathToItem == null) + { + throw new InvalidCmisObjectException("Invalid path set for content"); + } + CmisObject cmisObject = cmisAPI.getSession().getObjectByPath(Utility.removeLastSlash(pathToItem), context); + if (cmisObject instanceof Document) + { + if (!((Document) cmisObject).getVersionLabel().contentEquals("pwc")) + { + // get last version of document + cmisObject = ((Document) cmisObject).getObjectOfLatestVersion(false, context); + } + else + { + // get pwc document + cmisObject = cmisAPI.getSession().getObject(((Document) cmisObject).getObjectOfLatestVersion(false, context).getVersionSeriesCheckedOutId()); + } + } + return cmisObject; + } + + /** + * Get Document object for a file + * + * @param path String path to document + * @return {@link Document} object + */ + public Document getCmisDocument(final String path) + { + LOG.debug("Get CMIS Document by path {}", path); + Document d = null; + CmisObject docObj = getCmisObject(path); + if (docObj instanceof Document) + { + d = (Document) docObj; + } + else if (docObj instanceof Folder) + { + throw new InvalidCmisObjectException("Content at " + path + " is not a file"); + } + return d; + } + + /** + * Get Folder object for a folder + * + * @param path String path to folder + * @return {@link Folder} object + */ + public Folder getCmisFolder(final String path) + { + Folder f = null; + CmisObject folderObj = getCmisObject(path); + if (folderObj instanceof Folder) + { + f = (Folder) folderObj; + } + else if (folderObj instanceof Document) + { + throw new InvalidCmisObjectException("Content at " + path + " is not a folder"); + } + return f; + } + + /** + * Helper method to get the contents of a stream + * + * @param stream + * @return + * @throws IORuntimeException + */ + protected String getContentAsString(ContentStream stream) + { + LOG.debug("Get Content as String {}", stream); + InputStream inputStream = stream.getStream(); + String result; + try + { + result = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + } + catch (IOException e) + { + throw new IORuntimeException(e); + } + IOUtils.closeQuietly(inputStream); + return result; + } + + /** + * Copy all the children of the source folder to the target folder + * + * @param sourceFolder + * @param targetFolder + */ + protected void copyChildrenFromFolder(Folder sourceFolder, Folder targetFolder) + { + for (Tree t : sourceFolder.getDescendants(-1)) + { + CmisObject obj = t.getItem(); + if (obj instanceof Document) + { + Document d = (Document) obj; + d.copy(targetFolder); + } + else if (obj instanceof Folder) + { + copyFolder((Folder) obj, targetFolder); + } + } + } + + /** + * Copy folder with all children + * + * @param sourceFolder source folder + * @param targetFolder target folder + * @return CmisObject of new created folder + */ + public CmisObject copyFolder(Folder sourceFolder, Folder targetFolder) + { + Map folderProperties = new HashMap(2); + folderProperties.put(PropertyIds.NAME, sourceFolder.getName()); + folderProperties.put(PropertyIds.OBJECT_TYPE_ID, sourceFolder.getBaseTypeId().value()); + Folder newFolder = targetFolder.createFolder(folderProperties); + copyChildrenFromFolder(sourceFolder, newFolder); + return newFolder; + } + + protected boolean isPrivateWorkingCopy() + { + boolean result; + try + { + result = getPWCDocument().isVersionSeriesPrivateWorkingCopy(); + } + catch (CmisVersioningException cmisVersioningException) + { + result = false; + } + return result; + } + + /** + * Returns the PWC (private working copy) ID of the document version series + */ + public Document getPWCDocument() + { + Document document = getCmisDocument(cmisAPI.getLastResource()); + String pwcId = document.getVersionSeriesCheckedOutId(); + if (pwcId != null) + { + return (Document) cmisAPI.getSession().getObject(pwcId); + } + else + { + throw new CmisVersioningException(String.format("Document %s is not checked out", document.getName())); + } + } + + public FileModel getPWCFileModel() + { + Document document = getPWCDocument(); + String[] pathTokens = cmisAPI.getLastResource().split("/"); + String path = ""; + for (int i = 0; i < pathTokens.length - 1; i++) + path = Utility.buildPath(path, pathTokens[i]); + path = Utility.buildPath(path, document.getName()); + + FileModel fileModel = new FileModel(); + fileModel.setName(document.getName()); + fileModel.setCmisLocation(path); + return fileModel; + } + + protected Folder getFolderParent() + { + return getCmisFolder(cmisAPI.getLastResource()).getFolderParent(); + } + + /** + * @return List of allowable actions for the current object + */ + protected List getAllowableActions() + { + return Lists.newArrayList(getCmisObject(cmisAPI.getLastResource()).getAllowableActions().getAllowableActions()); + } + + /** + * Returns the requested property. If the property is not available, null is returned + * + * @param propertyId + * @return CMIS Property + */ + protected Property getProperty(String propertyId) + { + CmisObject cmisObject = getCmisObject(cmisAPI.getLastResource()); + return cmisObject.getProperty(propertyId); + } + + protected List getRenditions() + { + OperationContext operationContext = cmisAPI.getSession().createOperationContext(); + operationContext.setRenditionFilterString("*"); + CmisObject obj = cmisAPI.getSession().getObjectByPath(cmisAPI.getLastResource(), operationContext); + obj.refresh(); + List renditions = obj.getRenditions(); + int retry = 0; + while ((renditions == null || renditions.isEmpty()) && retry < Utility.retryCountSeconds) + { + Utility.waitToLoopTime(1); + obj.refresh(); + renditions = obj.getRenditions(); + retry++; + } + return obj.getRenditions(); + } + + protected List getSecondaryTypes() + { + CmisObject obj = getCmisObject(cmisAPI.getLastResource()); + obj.refresh(); + return obj.getSecondaryTypes(); + } + + /** + * Get the children from a parent folder + * + * @return Map + */ + public Map getChildren() + { + String folderParent = cmisAPI.getLastResource(); + ItemIterable children = cmisAPI.withCMISUtil().getCmisFolder(folderParent).getChildren(); + Map contents = new HashMap(); + for (CmisObject o : children) + { + ContentModel content = new ContentModel(o.getName()); + content.setNodeRef(o.getId()); + content.setDescription(o.getDescription()); + content.setCmisLocation(Utility.buildPath(folderParent, o.getName())); + contents.put(content, o.getType()); + } + return contents; + } + + /** + * Gets the folder descendants starting with the current folder + * + * @param depth level of the tree that you want to go to + * - currentFolder + * -- file1.txt + * -- file2.txt + * -- folderB + * --- file3.txt + * --- file4.txt + * e.g. A depth of 1 will give you just the current folder descendants (file1.txt, file2.txt, folder1) + * e.g. A depth of -1 will return all the descendants (file1.txt, file2.txt, folder1, file3.txt and file4.txt) + */ + public List getFolderDescendants(int depth) + { + return getFolderTreeCmisObjects(getCmisFolder(cmisAPI.getLastResource()).getDescendants(depth)); + } + + /** + * Returns a list of Cmis objects for the provided Content Models + * + * @param contentModels + */ + public List getCmisObjectsFromContentModels(ContentModel... contentModels) + { + List cmisObjects = new ArrayList<>(); + for (ContentModel contentModel : contentModels) + cmisObjects.add(getCmisObject(contentModel.getCmisLocation())); + return cmisObjects; + } + + public ContentStream getContentStream(String content) + { + String fileName = getCmisDocument(cmisAPI.getLastResource()).getName(); + + return cmisAPI.getDataContentService().getContentStream(fileName, content); + } + + public Acl getAcls() + { + OperationContext context = cmisAPI.getSession().createOperationContext(); + context.setIncludeAcls(true); + return getCmisObject(cmisAPI.getLastResource(), context).getAcl(); + } + + /** + * Gets only the folder descendants for the {@link #getLastResource()} folder + * + * @param depth level of the tree that you want to go to + * - currentFolder + * -- folderB + * -- folderC + * --- folderD + * e.g. A depth of 1 will give you just the current folder descendants (folderB, folderC) + * e.g. A depth of -1 will return all the descendants (folderB, folderC, folderD) + */ + public List getFolderTree(int depth) + { + return getFolderTreeCmisObjects(getCmisFolder(cmisAPI.getLastResource()).getFolderTree(depth)); + } + + /** + * Helper method for getFolderTree and getFolderDescendants that cycles threw the folder descendants and returns List + */ + private List getFolderTreeCmisObjects(List> descendants) + { + List cmisObjectList = new ArrayList<>(); + for (Tree descendant : descendants) + { + cmisObjectList.add(descendant.getItem()); + cmisObjectList.addAll(descendant.getChildren().stream().map(Tree::getItem).collect(Collectors.toList())); + } + return cmisObjectList; + } + + protected List getAllDocumentVersions() + { + return getCmisDocument(cmisAPI.getLastResource()).getAllVersions(); + } + + public List getAllDocumentVersionsBy(OperationContext context) + { + return getCmisDocument(cmisAPI.getLastResource()).getAllVersions(context); + } + + public List getCheckedOutDocumentsFromSession() + { + return com.google.common.collect.Lists.newArrayList(cmisAPI.getSession().getCheckedOutDocs()); + } + + public List getCheckedOutDocumentsFromSession(OperationContext context) + { + return com.google.common.collect.Lists.newArrayList(cmisAPI.getSession().getCheckedOutDocs(context)); + } + + public List getCheckedOutDocumentsFromFolder() + { + Folder folder = cmisAPI.withCMISUtil().getCmisFolder(cmisAPI.getLastResource()); + return com.google.common.collect.Lists.newArrayList(folder.getCheckedOutDocs()); + } + + public List getCheckedOutDocumentsFromFolder(OperationContext context) + { + Folder folder = cmisAPI.withCMISUtil().getCmisFolder(cmisAPI.getLastResource()); + return com.google.common.collect.Lists.newArrayList(folder.getCheckedOutDocs(context)); + } + + protected boolean isCmisObjectContainedInCmisCheckedOutDocumentsList(CmisObject cmisObject, List cmisCheckedOutDocuments) + { + for (Document cmisCheckedOutDocument : cmisCheckedOutDocuments) + if (cmisObject.getId().split(";")[0].equals(cmisCheckedOutDocument.getId().split(";")[0])) + return true; + return false; + } + + public Map getProperties(ContentModel contentModel, String baseTypeId) + { + + Map properties = new HashMap(); + properties.put(PropertyIds.OBJECT_TYPE_ID, baseTypeId); + properties.put(PropertyIds.NAME, contentModel.getName()); + + // WebServices binding does not have SecondaryTypes and cannot be added to Object. + // cm:title and cm:description Policies + if (cmisAPI.getSession().getBinding().getBindingType().value().equals(BindingType.WEBSERVICES.value())) + { + return properties; + } + + List aspects = new ArrayList(); + aspects.add("P:cm:titled"); + properties.put(PropertyIds.SECONDARY_OBJECT_TYPE_IDS, aspects); + properties.put("cm:title", contentModel.getTitle()); + properties.put("cm:description", contentModel.getDescription()); + return properties; + } + + public OperationContext setIncludeAclContext() + { + OperationContext context = cmisAPI.getSession().createOperationContext(); + context.setIncludeAcls(true); + return context; + } + + public List createAce(UserModel user, UserRole role) + { + List addPermission = new ArrayList(); + addPermission.add(role.getRoleId()); + Ace addAce = cmisAPI.getSession().getObjectFactory().createAce(user.getUsername(), addPermission); + List addAces = new ArrayList(); + addAces.add(addAce); + return addAces; + } + + public List createAce(GroupModel group, UserRole role) + { + List addPermission = new ArrayList(); + addPermission.add(role.getRoleId()); + Ace addAce = cmisAPI.getSession().getObjectFactory().createAce(group.getDisplayName(), addPermission); + List addAces = new ArrayList(); + addAces.add(addAce); + return addAces; + } + + public List createAce(UserModel user, String... permissions) + { + List addAces = new ArrayList(); + RepositoryInfo repositoryInfo = cmisAPI.getSession().getRepositoryInfo(); + AclCapabilities aclCapabilities = repositoryInfo.getAclCapabilities(); + Map permissionMappings = aclCapabilities.getPermissionMapping(); + for (String perm : permissions) + { + STEP(String.format("%s Add permission '%s' for user %s ", CmisWrapper.STEP_PREFIX, perm, user.getUsername())); + PermissionMapping permissionMapping = permissionMappings.get(perm); + List permissionsList = permissionMapping.getPermissions(); + Ace addAce = cmisAPI.getSession().getObjectFactory().createAce(user.getUsername(), permissionsList); + addAces.add(addAce); + } + return addAces; + } + + public ObjectType getTypeDefinition() + { + CmisObject cmisObject = cmisAPI.withCMISUtil().getCmisObject(cmisAPI.getLastResource()); + return cmisAPI.getSession().getTypeDefinition(cmisObject.getBaseTypeId().value()); + } + + public ItemIterable getTypeChildren(String baseType, boolean includePropertyDefinitions) + { + STEP(String.format("%s Get type children for '%s' and includePropertyDefinitions set to '%s'", CmisWrapper.STEP_PREFIX, baseType, + includePropertyDefinitions)); + return cmisAPI.getSession().getTypeChildren(baseType, includePropertyDefinitions); + } + + public List> getTypeDescendants(String baseTypeId, int depth, boolean includePropertyDefinitions) + { + STEP(String.format("%s Get type descendants for '%s' with depth set to %d and includePropertyDefinitions set to '%s'", CmisWrapper.STEP_PREFIX, + baseTypeId, depth, includePropertyDefinitions)); + return cmisAPI.getSession().getTypeDescendants(baseTypeId, depth, includePropertyDefinitions); + } + + public String getObjectId(String pathToObject) + { + return getCmisObject(pathToObject).getId(); + } + + /** + * Update property for last resource cmis object + * + * @param propertyName String property name (e.g. cmis:name) + * @param propertyValue Object property value + */ + public void updateProperties(String propertyName, Object propertyValue) + { + Map props = new HashMap(); + props.put(propertyName, propertyValue); + getCmisObject(cmisAPI.getLastResource()).updateProperties(props); + } + + protected boolean isFolderInList(FolderModel folderModel, List folders) + { + for (FolderModel folder : folders) + { + if (folderModel.getName().equals(folder.getName())) + { + return true; + } + } + return false; + } + + protected boolean isFileInList(FileModel fileModel, List files) + { + for (FileModel file : files) + { + if (fileModel.getName().equals(file.getName())) + { + return true; + } + } + return false; + } + + protected boolean isContentInList(ContentModel contentModel, List contents) + { + for (ContentModel content : contents) + { + if (content.getName().equals(content.getName())) + { + return true; + } + } + return false; + } + + /** + * Get children folders from a parent folder + * + * @return List + */ + public List getFolders() + { + STEP(String.format("%s Get the folder children from '%s'", CmisWrapper.STEP_PREFIX, cmisAPI.getLastResource())); + Map children = getChildren(); + List folders = new ArrayList(); + for (Map.Entry entry : children.entrySet()) + { + if (entry.getValue().getId().equals(BaseTypeId.CMIS_FOLDER.value())) + { + FolderModel folder = new FolderModel(entry.getKey().getName()); + folder.setNodeRef(entry.getKey().getNodeRef()); + folder.setDescription(entry.getKey().getDescription()); + folder.setCmisLocation(entry.getKey().getCmisLocation()); + folder.setProtocolLocation(entry.getKey().getCmisLocation()); + folders.add(folder); + } + } + return folders; + } + + /** + * Get children documents from a parent folder + * + * @return List + */ + public List getFiles() + { + STEP(String.format("%s Get the file children from '%s'", CmisWrapper.STEP_PREFIX, cmisAPI.getLastResource())); + Map children = getChildren(); + List files = new ArrayList(); + for (Map.Entry entry : children.entrySet()) + { + if (entry.getValue().getId().equals(BaseTypeId.CMIS_DOCUMENT.value())) + { + FileModel file = new FileModel(entry.getKey().getName()); + file.setNodeRef(entry.getKey().getNodeRef()); + file.setDescription(entry.getKey().getDescription()); + file.setCmisLocation(entry.getKey().getCmisLocation()); + file.setProtocolLocation(entry.getKey().getCmisLocation()); + files.add(file); + } + } + return files; + } + + /* + * Get document(set as last resource) content + */ + public String getDocumentContent() + { + Utility.waitToLoopTime(2); + Document lastVersion = getCmisDocument(cmisAPI.getLastResource()); + lastVersion.refresh(); + LOG.info(String.format("Get the content from %s - node: %s", lastVersion.getName(), lastVersion.getId())); + ContentStream contentStream = lastVersion.getContentStream(); + String actualContent = ""; + if (contentStream != null) + { + actualContent = getContentAsString(contentStream); + } + else + { + lastVersion = getCmisDocument(cmisAPI.getLastResource()); + lastVersion.refresh(); + LOG.info(String.format("Retry get content stream for %s node: %s", lastVersion.getName(), lastVersion.getId())); + contentStream = lastVersion.getContentStream(); + if (contentStream != null) + { + actualContent = getContentAsString(contentStream); + } + } + if(actualContent.isEmpty()) + { + Utility.waitToLoopTime(2); + Document retryDoc = getCmisDocument(cmisAPI.getLastResource()); + retryDoc.refresh(); + LOG.info(String.format("Retry get content stream for %s node: %s", retryDoc.getName(), retryDoc.getId())); + contentStream = retryDoc.getContentStream(); + if (contentStream != null) + { + actualContent = getContentAsString(contentStream); + } + } + return actualContent; + } + + /** + * Get user noderef + * + * @param user {@link UserModel} + */ + public String getUserNodeRef(UserModel user) + { + String objectId = ""; + ItemIterable results = cmisAPI.getSession().query("select cmis:objectId from cm:person where cm:userName = '" + user.getUsername() + "'", + false); + for (QueryResult qResult : results) + { + PropertyData propData = qResult.getPropertyById("cmis:objectId"); + objectId = (String) propData.getFirstValue(); + } + return objectId; + } +} diff --git a/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/DocumentVersioning.java b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/DocumentVersioning.java new file mode 100644 index 0000000000..e772aec0a9 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/DocumentVersioning.java @@ -0,0 +1,137 @@ +package org.alfresco.cmis.dsl; + +import static org.alfresco.utility.report.log.Step.STEP; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.alfresco.cmis.CmisWrapper; +import org.apache.chemistry.opencmis.client.api.CmisObject; +import org.apache.chemistry.opencmis.client.api.Document; +import org.apache.chemistry.opencmis.client.api.OperationContext; +import org.testng.Assert; + +/** + * DSL utility for verifying a document version {@link Document} + */ +public class DocumentVersioning +{ + private CmisWrapper cmisWrapper; + private CmisObject cmisObject; + private boolean majorVersion; + private Object versionLabel; + private List versions; + + public DocumentVersioning(CmisWrapper cmisWrapper, CmisObject cmisObject) + { + this.cmisWrapper = cmisWrapper; + this.cmisObject = cmisObject; + } + + private DocumentVersioning withLatestMajorVersion() + { + this.majorVersion = true; + return this; + } + + private DocumentVersioning withLatestMinorVersion() + { + this.majorVersion = false; + return this; + } + + private Document getVersionOfDocument() + { + Document document = (Document) cmisObject; + if (versionLabel != null) + { + List documents = document.getAllVersions(); + for (Document documentVersion : documents) + if (documentVersion.getVersionLabel().equals(versionLabel.toString())) + return documentVersion; + } + else + { + return document.getObjectOfLatestVersion(majorVersion); + } + return document; + } + + private List getDocumentVersions(List documentList) + { + List versions = new ArrayList(); + for (Document document : documentList) + { + versions.add(document.getVersionLabel()); + } + return versions; + } + + public CmisWrapper assertVersionIs(Double expectedVersion) + { + STEP(String.format("%s Verify if document '%s' has version '%s'", cmisWrapper.getProtocolName(), cmisObject.getName(), expectedVersion)); + Assert.assertEquals(getVersionOfDocument().getVersionLabel(), expectedVersion.toString(), "File has version"); + return cmisWrapper; + } + + public CmisWrapper assertLatestMajorVersionIs(Double expectedVersion) + { + STEP(String.format("%s Verify if latest major version of document '%s' is '%s'", cmisWrapper.getProtocolName(), cmisObject.getName(), expectedVersion)); + Assert.assertEquals(withLatestMajorVersion().getVersionOfDocument().getVersionLabel(), expectedVersion.toString(), "File has version"); + return cmisWrapper; + } + + public CmisWrapper assertLatestMinorVersionIs(Double expectedVersion) + { + STEP(String.format("%s Verify if latest minor version of document '%s' is '%s'", cmisWrapper.getProtocolName(), cmisObject.getName(), expectedVersion)); + Assert.assertEquals(withLatestMinorVersion().getVersionOfDocument().getVersionLabel(), expectedVersion.toString(), "File has version"); + return cmisWrapper; + } + + public DocumentVersioning getAllDocumentVersions() + { + setVersions(getDocumentVersions(cmisWrapper.withCMISUtil().getAllDocumentVersions())); + return this; + } + + public CmisWrapper assertHasVersions(Object... versions) + { + setVersions(getDocumentVersions(cmisWrapper.withCMISUtil().getAllDocumentVersions())); + List documentVersions = getVersions(); + for (Object version : versions) + { + STEP(String.format("%s Verify if document '%s' has version '%s'", cmisWrapper.getProtocolName(), cmisObject.getName(), version)); + Assert.assertTrue(documentVersions.contains(version.toString()), + String.format("Document %s does not have version %s", cmisObject.getName(), version)); + } + return cmisWrapper; + } + + public DocumentVersioning getAllDocumentVersionsBy(OperationContext context) + { + setVersions(getDocumentVersions(cmisWrapper.withCMISUtil().getAllDocumentVersionsBy(context))); + return this; + } + + public CmisWrapper assertHasVersionsInOrder(Object... versions) + { + List documentVersions = getVersions(); + List expectedVersions = Arrays.asList(versions); + STEP(String.format("%s Verify if document '%s' has versions in this order '%s'", cmisWrapper.getProtocolName(), cmisObject.getName(), + Arrays.toString(expectedVersions.toArray()))); + Assert.assertTrue(documentVersions.toString().equals(expectedVersions.toString()), + String.format("Document %s does not have versions in this order %s", cmisObject.getName(), Arrays.toString(expectedVersions.toArray()))); + return cmisWrapper; + } + + public List getVersions() + { + return versions; + } + + public void setVersions(List versions) + { + this.versions = versions; + } +} diff --git a/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/JmxUtil.java b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/JmxUtil.java new file mode 100644 index 0000000000..40666d1cb1 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/JmxUtil.java @@ -0,0 +1,39 @@ +package org.alfresco.cmis.dsl; + +import org.alfresco.cmis.CmisWrapper; +import org.alfresco.utility.network.Jmx; +import org.alfresco.utility.network.JmxClient; +import org.alfresco.utility.network.JmxJolokiaProxyClient; + +/** + * DSL for interacting with JMX (using direct JMX call see {@link JmxClient} or {@link JmxJolokiaProxyClient} + */ +public class JmxUtil +{ + @SuppressWarnings("unused") + private CmisWrapper cmisWrapper; + private Jmx jmx; + private final String jmxAuditObjectName = "Alfresco:Type=Configuration,Category=Audit,id1=default"; + + public JmxUtil(CmisWrapper cmisWrapper, Jmx jmx) + { + this.cmisWrapper = cmisWrapper; + this.jmx = jmx; + } + + public void enableCMISAudit() throws Exception + { + if(jmx.readProperty(jmxAuditObjectName, "audit.enabled").equals(String.valueOf(false))) + { + jmx.writeProperty(jmxAuditObjectName, "audit.enabled", String.valueOf(true)); + } + jmx.writeProperty(jmxAuditObjectName, "audit.cmischangelog.enabled", String.valueOf(true)); + jmx.writeProperty(jmxAuditObjectName, "audit.alfresco-access.enabled", String.valueOf(true)); + } + + public void disableCMISAudit() throws Exception + { + jmx.writeProperty(jmxAuditObjectName, "audit.cmischangelog.enabled", String.valueOf(false)); + jmx.writeProperty(jmxAuditObjectName, "audit.alfresco-access.enabled", String.valueOf(false)); + } +} diff --git a/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/QueryExecutor.java b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/QueryExecutor.java new file mode 100644 index 0000000000..040e8eee85 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/QueryExecutor.java @@ -0,0 +1,287 @@ +package org.alfresco.cmis.dsl; + +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; + +import static org.alfresco.utility.Utility.checkObjectIsInitialized; +import static org.alfresco.utility.report.log.Step.STEP; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.StreamSupport; + +import org.alfresco.cmis.CmisWrapper; +import org.alfresco.utility.LogFactory; +import org.alfresco.utility.data.provider.XMLTestData; +import org.alfresco.utility.exception.TestConfigurationException; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestModel; +import org.apache.chemistry.opencmis.client.api.ItemIterable; +import org.apache.chemistry.opencmis.client.api.QueryResult; +import org.apache.chemistry.opencmis.client.api.Session; +import org.apache.chemistry.opencmis.commons.data.PropertyData; +import org.slf4j.Logger; +import org.testng.Assert; + +/** + * DSL for CMIS Queries + * This will also handle execution of CMIS queries + */ +public class QueryExecutor +{ + static Logger LOG = LogFactory.getLogger(); + + CmisWrapper cmisWrapper; + private long resultCount = -1; + private String currentQuery = ""; + private List results; + + public QueryExecutor(CmisWrapper cmisWrapper, String query) + { + this.cmisWrapper = cmisWrapper; + currentQuery = query; + } + + public QueryResultAssertion assertResultsCount() + { + resultCount = executeQuery(currentQuery).getPageNumItems(); + return new QueryResultAssertion(); + } + + public QueryResultAssertion assertColumnIsOrdered() + { + return assertValues(); + } + + public QueryResultAssertion assertColumnValuesRange() + { + return assertValues(); + } + + public QueryResultAssertion assertValues() + { + STEP("Sending query " + currentQuery); + results = StreamSupport.stream(executeQuery(currentQuery).spliterator(), false) + .collect(toList()); + resultCount = results.size(); + STEP("Received results " + results.stream().map(this::resultToString).collect(toList())); + return new QueryResultAssertion(); + } + + /** Try to return a useful string representation of the CMIS query result. */ + private String resultToString(QueryResult result) + { + if (result == null || result.getProperties() == null) + { + return "null"; + } + Optional> idProperty = result.getProperties().stream() + .filter(propertyData -> propertyData.getId().equals("cmis:objectId")) + .findFirst(); + return idProperty.map(PropertyData::getValues) + .map(values -> values.stream().map(Object::toString).collect(joining(","))) + .orElse(result.getProperties().toString()); + } + + private ItemIterable executeQuery(String query) + { + Session session = cmisWrapper.getSession(); + checkObjectIsInitialized(session, "You need to authenticate first using "); + + return session.query(query, false); + } + + /** + * Call getNodeRef on each test data item used in test and replace that with NODE_REF keywords in your Query + * + * @param testData + * @return + */ + public QueryExecutor applyNodeRefsFrom(XMLTestData testData) + { + List dataItems = extractKeywords("NODE_REF"); + if (dataItems.isEmpty()) + return this; + + List nodeRefs = new ArrayList(); + for (String dataItemName : dataItems) + { + currentQuery = currentQuery.replace(String.format("NODE_REF[%s]", dataItemName), "%s"); + TestModel model = testData.getTestDataItemWithId(dataItemName).getModel(); + if (model == null) + throw new TestConfigurationException("No TestData with ID: " + dataItemName + " found in your XML file."); + + if (model instanceof SiteModel) + { + nodeRefs.add(cmisWrapper.getDataContentService().usingAdmin().usingSite((SiteModel) model).getNodeRef()); + } + else if (model instanceof FolderModel) + { + nodeRefs.add(((FolderModel) model).getNodeRef()); + } + else if (model instanceof FileModel) + { + nodeRefs.add(((FileModel) model).getNodeRef()); + } + } + + try + { + currentQuery = String.format(currentQuery, nodeRefs.toArray()); + LOG.info("Injecting nodeRef IDs \n\tQuery: [{}]", currentQuery); + } + catch (Exception e) + { + throw new TestConfigurationException( + "You passed multiple keywords to your search query, please re-analyze your query search format: " + e.getMessage()); + } + return this; + } + + /** + * if you have in your search 'SELECT * from cmis:document where workspace://SpacesStore/NODE_REF[site1] or workspace://SpacesStore/NODE_REF[site2]' + * and pass key="NODE_REF" this method will get "site1" and "site2" as values + * + * @param key + * @return + * @throws TestConfigurationException + */ + private List extractKeywords(String key) throws TestConfigurationException + { + String[] lines = currentQuery.split(key); + List keywords = new ArrayList(); + + for (int i = 0; i < lines.length; i++) + { + if (lines[i].startsWith("[")) + { + String keyValue = ""; + for (int j = 1; j < lines[i].length() - 1; j++) + { + String tmp = Character.toString(lines[i].charAt(j)); + if (tmp.equals("]")) + break; + keyValue += tmp; + } + keywords.add(keyValue); + } + } + return keywords; + } + + public class QueryResultAssertion + { + public QueryResultAssertion hasLength(long expectedValue) + { + STEP(String.format("Verify that query: '%s' has %d results count returned", currentQuery, expectedValue)); + Assert.assertEquals(resultCount, expectedValue, showErrorMessage()); + return this; + } + + public QueryResultAssertion isGreaterThan(long expectedValue) + { + STEP(String.format("Verify that query: '%s' has more than %d results count returned", currentQuery, expectedValue)); + if (expectedValue <= resultCount) + Assert.fail(String.format("%s expected to have more than %d results, but found %d", showErrorMessage(), expectedValue, resultCount)); + + return this; + } + + public QueryResultAssertion isLowerThan(long expectedValue) + { + STEP(String.format("Verify that query: '%s' has more than %d results count returned", currentQuery, expectedValue)); + if (resultCount >= expectedValue) + Assert.fail(String.format("%s expected to have less than %d results, but found %d", showErrorMessage(), expectedValue, resultCount)); + + return this; + } + + public QueryResultAssertion isOrderedAsc(String queryName) + { + STEP(String.format("Verify that query: '%s' is returning ascending ordered values for column %s", currentQuery, queryName)); + List columnValues = new ArrayList<>(); + results.forEach((r) -> { + columnValues.add(r.getPropertyValueByQueryName(queryName)); + }); + List orderedColumnValues = columnValues.stream().sorted().collect(toList()); + Assert.assertEquals(columnValues, orderedColumnValues, + String.format("%s column values expected to be in ascendent order, but found %s", queryName, columnValues.toString())); + + return this; + + } + + public QueryResultAssertion isOrderedDesc(String queryName) + { + STEP(String.format("Verify that query: '%s' is returning descending ordered values for column %s", currentQuery, queryName)); + List columnValues = new ArrayList<>(); + results.forEach((r) -> { + columnValues.add(r.getPropertyValueByQueryName(queryName)); + }); + List reverseOrderedColumnValues = columnValues.stream().sorted(Collections.reverseOrder()).collect(toList()); + Assert.assertEquals(columnValues, reverseOrderedColumnValues, + String.format("%s column values expected to be in descendent order, but found %s", queryName, columnValues.toString())); + + return this; + + } + + public QueryResultAssertion isReturningValuesInRange(String queryName, BigDecimal minValue, BigDecimal maxValue) + { + STEP(String.format("Verify that query: '%s' is returning values for column %s in range from %.4f to %.4f", currentQuery, queryName, minValue, maxValue)); + results.forEach((r) -> { + BigDecimal value = (BigDecimal) r.getPropertyValueByQueryName(queryName); + if (value.compareTo(minValue) < 0 || value.compareTo(maxValue) > 0) + { + Assert.fail(String.format("%s column values expected to be in range from %.4f to %.4f, but found %.4f", queryName, minValue, maxValue, value)); + } + }); + + return this; + } + + public QueryResultAssertion isReturningValues(String queryName, Set values) + { + return isReturningValues(queryName, values, false); + } + + public QueryResultAssertion isReturningValues(String queryName, Set values, boolean multivalue) + { + STEP(String.format("Verify that query: '%s' returns the values from %s for column %s", currentQuery, values, queryName)); + Function extractValue = (multivalue ? (r -> r.getPropertyMultivalueById(queryName)) : r -> r.getPropertyValueById(queryName)); + Set resultSet = results.stream().map(extractValue).collect(toSet()); + Assert.assertEquals(resultSet, values, "Values did not match - expected " + values + " got " + resultSet); + + return this; + } + + public QueryResultAssertion isReturningOrderedValues(String queryName, List values) + { + return isReturningOrderedValues(queryName, values, false); + } + + public QueryResultAssertion isReturningOrderedValues(String queryName, List values, boolean multivalue) + { + STEP(String.format("Verify that query: '%s' returns the values from %s for column %s", currentQuery, values, queryName)); + Function extractValue = (multivalue ? (r -> r.getPropertyMultivalueById(queryName)) : r -> r.getPropertyValueById(queryName)); + List resultList = results.stream().map(extractValue).collect(toList()); + // Include both lists in assertion message as TestNG does not provide this information. + Assert.assertEquals(resultList, values, "Values did not match expected " + values + " but found " + resultList); + + return this; + } + + private String showErrorMessage() + { + return String.format("Returned results count of Query [%s] is not the expected one:", currentQuery); + } + } +} diff --git a/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/exception/InvalidCmisObjectException.java b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/exception/InvalidCmisObjectException.java new file mode 100644 index 0000000000..9bc532652d --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/exception/InvalidCmisObjectException.java @@ -0,0 +1,10 @@ +package org.alfresco.cmis.exception; + +public class InvalidCmisObjectException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + public InvalidCmisObjectException(String reason) + { + super(reason); + } +} diff --git a/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/exception/UnrecognizedBinding.java b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/exception/UnrecognizedBinding.java new file mode 100644 index 0000000000..cf1dcaf250 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/exception/UnrecognizedBinding.java @@ -0,0 +1,12 @@ +package org.alfresco.cmis.exception; + +public class UnrecognizedBinding extends Exception +{ + private static final long serialVersionUID = 1L; + private static final String DEFAULT_MESSAGE = "Unrecognized CMIS Binding [%s]. Available binding options: BROWSER or ATOM"; + + public UnrecognizedBinding(String binding) + { + super(String.format(DEFAULT_MESSAGE, binding)); + } +} diff --git a/packaging/tests/tas-cmis/src/main/resources/alfresco-cmis-context.xml b/packaging/tests/tas-cmis/src/main/resources/alfresco-cmis-context.xml new file mode 100644 index 0000000000..ecf3d2db4b --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/resources/alfresco-cmis-context.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/packaging/tests/tas-cmis/src/main/resources/default.properties b/packaging/tests/tas-cmis/src/main/resources/default.properties new file mode 100644 index 0000000000..1f69d79d08 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/resources/default.properties @@ -0,0 +1,76 @@ +# dataprep related +alfresco.scheme=http +alfresco.server=localhost +alfresco.port=8081 + +# credentials +admin.user=admin +admin.password=admin + +solrWaitTimeInSeconds=30 + +# in containers we cannot access directly JMX, so we will use http://jolokia.org agent +# disabling this we will use direct JMX calls to server +jmx.useJolokiaAgent=false + +# Server Health section +# in ServerHealth#isServerReachable() - could also be shown. +# enable this option to view if on server there are tenants or not +serverHealth.showTenants=true + +# set CMIS binding to 'browser' or 'atom' +cmis.binding=browser +cmis.basePath=/alfresco/api/-default-/public/cmis/versions/1.1/${cmis.binding} + +# TEST MANAGEMENT SECTION - Test Rail +# +# (currently supporting Test Rail v5.2.1.3472 integration) +# +# Example of configuration: +# ------------------------------------------------------ +# if testManagement.enabled=true we enabled TestRailExecutorListener (if used in your suite xml file) +# testManagement.updateTestExecutionResultsOnly=true (this will just update the results of a test: no step will be updated - good for performance) +# testManagement.endPoint=https://alfresco.testrail.com/ +# testManagement.username= +# testManagement.apiKey= +# testManagement.project= +# testManagement.includeOnlyTestCasesExecuted=true #if you want to include in your run ONLY the test cases that you run, then set this value to false +# testManagement.rateLimitInSeconds=1 #is the default rate limit after what minimum time, should we upload the next request. http://docs.gurock.com/testrail-api2/introduction #Rate Limit +# testManagement.suiteId=23 (the id of the Master suite) +# ------------------------------------------------------ +testManagement.enabled=false +testManagement.endPoint= +testManagement.username= +testManagement.apiKey= +testManagement.project=7 +testManagement.includeOnlyTestCasesExecuted=true +testManagement.rateLimitInSeconds=1 +testManagement.testRun=MyTestRunInTestRail +testManagement.suiteId=12 + +# The location of the reports path +reports.path=./target/reports + +# +# Database Section +# You should provide here the database URL, that can be a differed server as alfresco. +# https://docs.oracle.com/javase/tutorial/jdbc/basics/connecting.html +# +# Current supported db.url: +# +# MySQL: +# db.url = jdbc:mysql://${alfresco.server}:3306/alfresco +# +# PostgreSQL: +# db.url = jdbc:postgresql://:3306/alfresco +# +# Oracle: +# db.url = jdbc:oracle://:3306/alfresco +# +# MariaDB: +# db.url = jdbc:mariadb://:3306/alfresco +# +db.url = jdbc:mysql://${alfresco.server}:3306/alfresco +db.username = alfresco +db.password = alfresco diff --git a/packaging/tests/tas-cmis/src/main/resources/log4j.properties b/packaging/tests/tas-cmis/src/main/resources/log4j.properties new file mode 100644 index 0000000000..00e9b5a114 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/resources/log4j.properties @@ -0,0 +1,26 @@ +# Root logger option +log4j.rootLogger=INFO, file, stdout + +# Direct log messages to a log file +log4j.appender.file=org.apache.log4j.RollingFileAppender +log4j.appender.file.File=./target/reports/alfresco-tas.log +log4j.appender.file.MaxBackupIndex=10 +log4j.appender.file.layout=org.apache.log4j.PatternLayout +log4j.appender.file.layout.ConversionPattern=[%t] %d{HH:mm:ss} %-5p %c{1}:%L - %m%n + +# Direct log messages to stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=[%t] %d{HH:mm:ss} %-5p %c{1}:%L - %m%n + +# TestRail particular log file +# Direct log messages to a log file +log4j.appender.testrailLog=org.apache.log4j.RollingFileAppender +log4j.appender.testrailLog.File=./target/reports/alfresco-testrail.log +log4j.appender.testrailLog.MaxBackupIndex=10 +log4j.appender.testrailLog.layout=org.apache.log4j.PatternLayout +log4j.appender.testrailLog.layout.ConversionPattern=%d{HH:mm:ss} %-5p %c{1}:%L - %m%n + +log4j.category.testrail=INFO, testrailLog +log4j.additivity.testrail=false \ No newline at end of file diff --git a/packaging/tests/tas-cmis/src/test/java/org/alfresco/cmis/search/AbstractCmisE2ETest.java b/packaging/tests/tas-cmis/src/test/java/org/alfresco/cmis/search/AbstractCmisE2ETest.java index df41d989ae..9083f303da 100644 --- a/packaging/tests/tas-cmis/src/test/java/org/alfresco/cmis/search/AbstractCmisE2ETest.java +++ b/packaging/tests/tas-cmis/src/test/java/org/alfresco/cmis/search/AbstractCmisE2ETest.java @@ -1,9 +1,21 @@ package org.alfresco.cmis.search; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; + +import static org.alfresco.utility.report.log.Step.STEP; + import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; import org.alfresco.cmis.CmisProperties; +import org.alfresco.cmis.dsl.QueryExecutor.QueryResultAssertion; import org.alfresco.utility.Utility; +import org.alfresco.utility.model.ContentModel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -45,32 +57,76 @@ public abstract class AbstractCmisE2ETest extends AbstractE2EFunctionalTest /** * Repeat Elastic Query till results count returns expectedCountResults * @param query CMIS Query to be executed - * @param expectedCountResults Number of results expected + * @param expectedResultsCount Number of results expected * @return true when results count is equals to expectedCountResults */ - protected boolean waitForIndexing(String query, long expectedCountResults) + protected boolean waitForIndexing(String query, long expectedResultsCount) { - - for (int searchCount = 1; searchCount <= SEARCH_MAX_ATTEMPTS; searchCount++) + try { + waitForIndexing(query, execution -> execution.hasLength(expectedResultsCount)); + return true; + } + catch (AssertionError ae) + { + STEP("Received assertion error for query '" + query + "': " + ae); + return false; + } + } + /** + * Repeat Elastic Query until we get the expected results or we hit the retry limit. + * + * @param query CMIS Query to be executed + * @param expectedResults The expected results (unordered). + */ + protected void waitForIndexing(String query, ContentModel... expectedResults) + { + Set expectedNames = Arrays.stream(expectedResults).map(ContentModel::getName).collect(toSet()); + waitForIndexing(query, execution -> execution.isReturningValues("cmis:name", expectedNames)); + } + + /** + * Repeat Elastic Query until we get the expected results in the given order or we hit the retry limit. + * + * @param query CMIS Query to be executed + * @param expectedResults The expected results (ordered). + */ + protected void waitForIndexingOrdered(String query, ContentModel... expectedResults) + { + List expectedNames = Arrays.stream(expectedResults).map(ContentModel::getName).collect(toList()); + waitForIndexing(query, execution -> execution.isReturningOrderedValues("cmis:name", expectedNames)); + } + + /** + * Repeat Elastic Query until we get the expected results or we hit the retry limit. + * + * @param query CMIS Query to be executed + * @param assertionMethod A method that will be called to check the response and which will throw an AssertionError if they aren't what we want. + */ + protected void waitForIndexing(String query, Consumer assertionMethod) + { + int searchCount = 0; + while (true) + { try { - cmisApi.withQuery(query).assertResultsCount().equals(expectedCountResults); - return true; + assertionMethod.accept(cmisApi.withQuery(query).assertValues()); + return; } catch (AssertionError ae) { - LOGGER.info(String.format("WaitForIndexing in Progress: %s", ae)); + searchCount++; + if (searchCount < SEARCH_MAX_ATTEMPTS) + { + LOGGER.info(String.format("WaitForIndexing in Progress: %s", ae)); + Utility.waitToLoopTime(getElasticWaitTimeInSeconds(), "Wait For Indexing"); + } + else + { + throw ae; + } } - - - Utility.waitToLoopTime(getElasticWaitTimeInSeconds(), "Wait For Indexing"); - } - - return false; } - - } diff --git a/packaging/tests/tas-cmis/src/test/java/org/alfresco/cmis/search/SearchInFolderTests.java b/packaging/tests/tas-cmis/src/test/java/org/alfresco/cmis/search/SearchInFolderTests.java index edd25d9a8f..5ef1738470 100644 --- a/packaging/tests/tas-cmis/src/test/java/org/alfresco/cmis/search/SearchInFolderTests.java +++ b/packaging/tests/tas-cmis/src/test/java/org/alfresco/cmis/search/SearchInFolderTests.java @@ -1,12 +1,12 @@ package org.alfresco.cmis.search; +import java.util.List; +import java.util.Set; + import org.alfresco.utility.Utility; -import org.alfresco.utility.data.provider.XMLDataConfig; -import org.alfresco.utility.data.provider.XMLTestDataProvider; import org.alfresco.utility.model.FileModel; import org.alfresco.utility.model.FileType; import org.alfresco.utility.model.FolderModel; -import org.alfresco.utility.model.QueryModel; import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; @@ -17,6 +17,21 @@ public class SearchInFolderTests extends AbstractCmisE2ETest private FolderModel parentFolder, subFolder1, subFolder2, subFolder3; private FileModel subFile1, subFile2, subFile3, subFile4, subFile5; + /** + * Create test data in the following format: + *
+     * testSite
+     * +- parentFolder
+     *    +- subFile5 (fifthFile.txt: "fifthFile content")
+     *    +- subFolder1
+     *    +- subFolder2
+     *    +- subFolder3 (subFolder)
+     *    +- subFile1 (firstFile.xls)
+     *    +- subFile2 (.pptx)
+     *    +- subFile3 (.txt)
+     *    +- subFile4 (fourthFile.docx: "fourthFileTitle", "fourthFileDescription")
+     * 
+ */ @BeforeClass(alwaysRun = true) public void createTestData() throws Exception { @@ -51,12 +66,164 @@ public class SearchInFolderTests extends AbstractCmisE2ETest dataContent.deleteSite(testSite); } - @Test(dataProviderClass = XMLTestDataProvider.class, dataProvider = "getQueriesData") - @XMLDataConfig(file = "src/test/resources/search-in-folder.xml") - public void executeCMISQuery(QueryModel query) + @Test + public void executeCMISQuery_selectFieldsFromFolder() { - String currentQuery = String.format(query.getValue(), parentFolder.getNodeRef()); + String query = "SELECT cmis:name, cmis:parentId, cmis:path, cmis:allowedChildObjectTypeIds" + + " FROM cmis:folder where IN_FOLDER('%s') AND cmis:name = 'subFolder'"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); cmisApi.authenticateUser(testUser); - Assert.assertTrue(waitForIndexing(currentQuery, query.getResults()), String.format("Result count not as expected for query: %s", currentQuery)); + waitForIndexing(currentQuery, subFolder3); + } + + @Test + public void executeCMISQuery_selectFieldsFromDocument() + { + String query = "SELECT cmis:name, cmis:objectId, cmis:lastModifiedBy, cmis:creationDate, cmis:contentStreamFileName" + + " FROM cmis:document where IN_FOLDER('%s') AND cmis:name = 'fourthFile'"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); + cmisApi.authenticateUser(testUser); + waitForIndexing(currentQuery, subFile4); + } + + @Test + public void executeCMISQuery_selectParentId() + { + String query = "SELECT cmis:parentId FROM cmis:folder where IN_FOLDER('%s')"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); + cmisApi.authenticateUser(testUser); + // Expect to get the same parent for each of the three matches. + String parentId = parentFolder.getNodeRef(); + List expectedParentIds = List.of(parentId, parentId, parentId); + waitForIndexing(currentQuery, execution -> execution.isReturningOrderedValues("cmis:parentId", expectedParentIds)); + } + + @Test + public void executeCMISQuery_inFolder() + { + String query = "SELECT * FROM cmis:document where IN_FOLDER('%s')"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); + cmisApi.authenticateUser(testUser); + waitForIndexing(currentQuery, subFile1, subFile2, subFile3, subFile4, subFile5); + } + + @Test + public void executeCMISQuery_orderByNameAsc() + { + String query = "SELECT * FROM cmis:document where IN_FOLDER('%s') AND cmis:name NOT LIKE 'file%%' ORDER BY cmis:name ASC"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); + cmisApi.authenticateUser(testUser); + waitForIndexingOrdered(currentQuery, subFile5, subFile1, subFile4); + } + + @Test + public void executeCMISQuery_orderByNameDesc() + { + String query = "SELECT * FROM cmis:document where IN_FOLDER('%s') AND cmis:name NOT LIKE 'file%%' ORDER BY cmis:name DESC"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); + cmisApi.authenticateUser(testUser); + waitForIndexingOrdered(currentQuery, subFile4, subFile1, subFile5); + } + + @Test + public void executeCMISQuery_orderByLastModifiedAsc() + { + String query = "SELECT * FROM cmis:folder where IN_FOLDER('%s') ORDER BY cmis:lastModificationDate ASC"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); + cmisApi.authenticateUser(testUser); + waitForIndexingOrdered(currentQuery, subFolder1, subFolder2, subFolder3); + } + + @Test + public void executeCMISQuery_orderByLastModifiedDesc() + { + String query = "SELECT * FROM cmis:folder where IN_FOLDER('%s') ORDER BY cmis:lastModificationDate DESC"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); + cmisApi.authenticateUser(testUser); + waitForIndexingOrdered(currentQuery, subFolder3, subFolder2, subFolder1); + } + + @Test + public void executeCMISQuery_orderByCreatedBy() + { + String query = "SELECT * FROM cmis:document where IN_FOLDER('%s') ORDER BY cmis:createdBy DESC"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); + cmisApi.authenticateUser(testUser); + // All the results were created by the same user, so we can't assert anything about the order. + waitForIndexing(currentQuery, subFile5, subFile1, subFile2, subFile3, subFile4); + } + + @Test + public void executeCMISQuery_documentNameNotNull() + { + String query = "SELECT * FROM cmis:document where IN_FOLDER('%s') AND cmis:name IS NOT NULL"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); + cmisApi.authenticateUser(testUser); + waitForIndexing(currentQuery, subFile1, subFile2, subFile3, subFile4, subFile5); + } + + @Test + public void executeCMISQuery_folderNameNotNull() + { + String query = "SELECT * FROM cmis:folder where IN_FOLDER('%s') AND cmis:name IS NOT NULL"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); + cmisApi.authenticateUser(testUser); + waitForIndexing(currentQuery, subFolder1, subFolder2, subFolder3); + } + + @Test + public void executeCMISQuery_nameLike() + { + String query = "SELECT * FROM cmis:document where IN_FOLDER('%s') AND cmis:name LIKE 'fourthFile'"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); + cmisApi.authenticateUser(testUser); + waitForIndexingOrdered(currentQuery, subFile4); + } + + @Test + public void executeCMISQuery_doubleNegative() + { + String query = "SELECT * FROM cmis:folder where IN_FOLDER('%s') AND NOT(cmis:name NOT IN ('subFolder'))"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); + cmisApi.authenticateUser(testUser); + waitForIndexingOrdered(currentQuery, subFolder3); + } + + @Test + public void executeCMISQuery_nameInList() + { + String query = "SELECT * FROM cmis:document where IN_FOLDER('%s') AND cmis:name IN ('fourthFile', 'fifthFile.txt')"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); + cmisApi.authenticateUser(testUser); + waitForIndexing(currentQuery, subFile4, subFile5); + } + + @Test + public void executeCMISQuery_nameNotInList() + { + String query = "SELECT * FROM cmis:document where IN_FOLDER('%s') AND cmis:name NOT IN ('fourthFile', 'fifthFile.txt')"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); + cmisApi.authenticateUser(testUser); + waitForIndexing(currentQuery, subFile1, subFile2, subFile3); + } + + @Test + public void executeCMISQuery_nameDifferentFrom() + { + String query = "SELECT * FROM cmis:folder where IN_FOLDER('%s') AND cmis:name <> 'subFolder'"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); + cmisApi.authenticateUser(testUser); + waitForIndexing(currentQuery, subFolder1, subFolder2); + } + + @Test + public void executeCMISQuery_selectSecondaryObjectTypeIds() + { + String query = "SELECT cmis:secondaryObjectTypeIds FROM cmis:folder where IN_FOLDER('%s') AND cmis:name = 'subFolder'"; + String currentQuery = String.format(query, parentFolder.getNodeRef()); + cmisApi.authenticateUser(testUser); + Set> expectedSecondaryObjectTypeIds = Set.of(List.of("P:cm:titled", "P:sys:localized")); + waitForIndexing(currentQuery, execution -> execution.isReturningValues("cmis:secondaryObjectTypeIds", expectedSecondaryObjectTypeIds, true)); + Assert.assertTrue(waitForIndexing(currentQuery, 1), String.format("Result count not as expected for query: %s", currentQuery)); } } diff --git a/packaging/tests/tas-email/pom.xml b/packaging/tests/tas-email/pom.xml index 189b20fb79..4faa117795 100644 --- a/packaging/tests/tas-email/pom.xml +++ b/packaging/tests/tas-email/pom.xml @@ -9,7 +9,7 @@ org.alfresco alfresco-community-repo-tests - 17.43-SNAPSHOT + 20.5-SNAPSHOT diff --git a/packaging/tests/tas-integration/pom.xml b/packaging/tests/tas-integration/pom.xml index caeb720922..a67032ad81 100644 --- a/packaging/tests/tas-integration/pom.xml +++ b/packaging/tests/tas-integration/pom.xml @@ -9,7 +9,7 @@ org.alfresco alfresco-community-repo-tests - 17.43-SNAPSHOT + 20.5-SNAPSHOT @@ -73,6 +73,7 @@ org.alfresco.tas cmis + ${project.version} test @@ -106,8 +107,10 @@ ${suiteXmlFile} + --illegal-access=warn + --add-opens=java.base/java.lang=ALL-UNNAMED diff --git a/packaging/tests/tas-restapi/pom.xml b/packaging/tests/tas-restapi/pom.xml index c6cbce1613..15ff17b29b 100644 --- a/packaging/tests/tas-restapi/pom.xml +++ b/packaging/tests/tas-restapi/pom.xml @@ -9,7 +9,7 @@ org.alfresco alfresco-community-repo-tests - 17.43-SNAPSHOT + 20.5-SNAPSHOT @@ -78,8 +78,10 @@ ${suiteXmlFile} + --illegal-access=warn + --add-opens=java.base/java.lang=ALL-UNNAMED diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/RestTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/RestTest.java index f4104eda88..92fff70016 100644 --- a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/RestTest.java +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/RestTest.java @@ -5,6 +5,7 @@ import java.lang.reflect.Method; import org.alfresco.dataprep.WorkflowService; import org.alfresco.rest.core.RestProperties; import org.alfresco.rest.core.RestWrapper; +import org.alfresco.rest.rules.RulesTestsUtils; import org.alfresco.utility.LogFactory; import org.alfresco.utility.TasProperties; import org.alfresco.utility.data.DataContent; @@ -61,6 +62,9 @@ public abstract class RestTest extends AbstractTestNGSpringContextTests @Autowired protected WorkflowService workflow; + @Autowired + protected RulesTestsUtils rulesUtils; + protected SiteModel testSite; @BeforeSuite(alwaysRun = true) diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/ActionsTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/ActionsTests.java index df822111de..fdfe88c263 100644 --- a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/ActionsTests.java +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/ActionsTests.java @@ -2,10 +2,13 @@ package org.alfresco.rest.actions; import static org.testng.Assert.assertFalse; -import com.google.common.collect.ImmutableMap; +import java.util.List; +import com.google.common.collect.ImmutableMap; import org.alfresco.dataprep.CMISUtil; import org.alfresco.rest.RestTest; +import org.alfresco.rest.model.RestActionConstraintDataModel; +import org.alfresco.rest.model.RestActionConstraintModel; import org.alfresco.rest.model.RestActionDefinitionModel; import org.alfresco.rest.model.RestActionDefinitionModelsCollection; import org.alfresco.rest.model.RestNodeModel; @@ -148,4 +151,66 @@ public class ActionsTests extends RestTest restActionDefinition.getDescription().equals("This will add an aspect to the matched item."); restActionDefinition.getTitle().equals("Add aspect"); } + + @TestRail(section = {TestGroup.REST_API, TestGroup.ACTIONS}, executionType = ExecutionType.SANITY, + description = "Sanity test for ACTIONS endpoint GET action-conditions/{actionConstraintName}") + @Test(groups = {TestGroup.REST_API, TestGroup.ACTIONS, TestGroup.SANITY}) + public void testGetSingleActionConstraint() + { + final UserModel testUser = dataUser.createRandomTestUser(); + restClient.authenticateUser(testUser); + + final String compareOperationsName = "ac-compare-operations"; + final RestActionConstraintModel actionConstraintCompareOperations = + restClient.withCoreAPI().usingActions().getActionConstraintByName(compareOperationsName); + + restClient.assertStatusCodeIs(HttpStatus.OK); + + final RestActionConstraintModel expectedComparatorConstraints = new RestActionConstraintModel(); + expectedComparatorConstraints.setConstraintName(compareOperationsName); + expectedComparatorConstraints.setConstraintValues(getComparatorConstraints()); + actionConstraintCompareOperations.assertThat().isEqualTo(expectedComparatorConstraints); + } + + @TestRail(section = {TestGroup.REST_API, TestGroup.ACTIONS}, executionType = ExecutionType.SANITY, + description = "Sanity test for ACTIONS endpoint GET action-conditions/{actionConstraintName} - non existing constraint name") + @Test(groups = {TestGroup.REST_API, TestGroup.ACTIONS, TestGroup.SANITY}) + public void testGetSingleNonExistingActionConstraint() + { + final UserModel testUser = dataUser.createRandomTestUser(); + restClient.authenticateUser(testUser); + restClient.withCoreAPI().usingActions().getActionConstraintByName("dummy-name"); + + restClient.assertStatusCodeIs(HttpStatus.NOT_FOUND); + } + + private List getComparatorConstraints() + { + final RestActionConstraintDataModel equalsConstraint = new RestActionConstraintDataModel(); + equalsConstraint.setValue("EQUALS"); + equalsConstraint.setLabel("Equals"); + final RestActionConstraintDataModel containsConstraint = new RestActionConstraintDataModel(); + containsConstraint.setValue("CONTAINS"); + containsConstraint.setLabel("Contains"); + final RestActionConstraintDataModel beginsConstraint = new RestActionConstraintDataModel(); + beginsConstraint.setValue("BEGINS"); + beginsConstraint.setLabel("Begins With"); + final RestActionConstraintDataModel endsConstraint = new RestActionConstraintDataModel(); + endsConstraint.setValue("ENDS"); + endsConstraint.setLabel("Ends With"); + final RestActionConstraintDataModel greaterThanConstraint = new RestActionConstraintDataModel(); + greaterThanConstraint.setValue("GREATER_THAN"); + greaterThanConstraint.setLabel("Greater Than"); + final RestActionConstraintDataModel greaterThanEqualConstraint = new RestActionConstraintDataModel(); + greaterThanEqualConstraint.setValue("GREATER_THAN_EQUAL"); + greaterThanEqualConstraint.setLabel("Greater Than Or Equal To"); + final RestActionConstraintDataModel lessThanConstraint = new RestActionConstraintDataModel(); + lessThanConstraint.setValue("LESS_THAN"); + lessThanConstraint.setLabel("Less Than"); + final RestActionConstraintDataModel lessThanEqualConstraint = new RestActionConstraintDataModel(); + lessThanEqualConstraint.setValue("LESS_THAN_EQUAL"); + lessThanEqualConstraint.setLabel("Less Than Or Equal To"); + return List.of(equalsConstraint, containsConstraint, beginsConstraint, endsConstraint, greaterThanConstraint, + greaterThanEqualConstraint, lessThanConstraint, lessThanEqualConstraint); + } } diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/AccessRestrictionUtil.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/AccessRestrictionUtil.java new file mode 100644 index 0000000000..50d127f96f --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/AccessRestrictionUtil.java @@ -0,0 +1,104 @@ +package org.alfresco.rest.actions.access; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gson.Gson; + +import org.alfresco.rest.actions.access.pojo.Action; +import org.alfresco.rest.actions.access.pojo.ActionCondition; +import org.alfresco.rest.actions.access.pojo.Rule; +import org.alfresco.utility.model.UserModel; + +public class AccessRestrictionUtil { + + public static final String MAIL_ACTION = "mail"; + + public static final String ERROR_MESSAGE_FIELD = "message"; + public static final String ERROR_MESSAGE_ACCESS_RESTRICTED = + "Only admin or system user is allowed to define uses of or directly execute this action"; + private static final String ERROR_MESSAGE_FAILED_TO_SEND_EMAIL = "Failed to send email to:"; + + public static Map createMailParameters(UserModel sender, UserModel recipient) { + Map parameterValues = new HashMap<>(); + parameterValues.put("from", sender.getEmailAddress()); + parameterValues.put("to", recipient.getEmailAddress()); + parameterValues.put("subject", "Test"); + parameterValues.put("text", "content"); + + return parameterValues; + } + + public static Rule createRuleWithAction(String actionName, Map parameterValues) { + Rule rule = new Rule(); + rule.setId(""); + rule.setTitle("Test rule title"); + rule.setDescription("Test rule description"); + rule.setRuleType(List.of("inbound")); + rule.setDisabled(false); + rule.setApplyToChildren(false); + rule.setExecuteAsynchronously(false); + + Action compositeAction = new Action(); + compositeAction.setActionDefinitionName("composite-action"); + + ActionCondition actionCondition = new ActionCondition(); + actionCondition.setConditionDefinitionName("no-condition"); + actionCondition.setParameterValues(new HashMap<>()); + + compositeAction.setConditions(List.of(actionCondition)); + + Action action = createAction(actionName, parameterValues); + + compositeAction.setActions(List.of(action)); + + rule.setAction(compositeAction); + + return rule; + } + + public static Action createActionWithParameters(String actionName, Map parameterValues) { + Action compositeAction = new Action(); + compositeAction.setActionDefinitionName("composite-action"); + + ActionCondition actionCondition = new ActionCondition(); + actionCondition.setConditionDefinitionName("no-condition"); + actionCondition.setParameterValues(new HashMap<>()); + + compositeAction.setConditions(List.of(actionCondition)); + + Action action = createAction(actionName, parameterValues); + action.setExecuteAsynchronously(false); + + compositeAction.setActions(List.of(action)); + + return action; + } + + + public static Action createAction(String actionName, Map parameterValues) { + Action action = new Action(); + action.setActionDefinitionName(actionName); + action.setParameterValues(parameterValues); + + return action; + } + + public static String mapObjectToJSON(Object object) { + Gson gson = new Gson(); + return gson.toJson(object); + } + + /** + * Return error message that in fact means that the action has passed access restriction correctly, + * but due to non-configured smtp couldn't send email. + * + * @param userModel + * @return + */ + public static String getExpectedEmailSendFailureMessage(UserModel userModel) { + return ERROR_MESSAGE_FAILED_TO_SEND_EMAIL + userModel.getEmailAddress(); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/FormProcAdminAccessRestrictionTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/FormProcAdminAccessRestrictionTest.java new file mode 100644 index 0000000000..a246ecf2f4 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/FormProcAdminAccessRestrictionTest.java @@ -0,0 +1,85 @@ +package org.alfresco.rest.actions.access; + +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.ERROR_MESSAGE_ACCESS_RESTRICTED; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.ERROR_MESSAGE_FIELD; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.MAIL_ACTION; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.createMailParameters; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.getExpectedEmailSendFailureMessage; +import static org.hamcrest.Matchers.containsString; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.rest.RestTest; +import org.alfresco.rest.core.RestRequest; +import org.alfresco.rest.core.RestResponse; +import org.alfresco.rest.core.RestWrapper; +import org.alfresco.utility.model.UserModel; +import org.json.simple.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class FormProcAdminAccessRestrictionTest extends RestTest { + + private static final String ACTION_FORM_PROCESSOR_ENDPOINT = "alfresco/service/api/action/%s/formprocessor"; + + private static final String PROPERTY_PREFIX = "prop_"; + + private UserModel adminUser; + private UserModel testUser; + + @Autowired + protected RestWrapper restClient; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception { + adminUser = dataUser.getAdminUser(); + testUser = dataUser.createRandomTestUser(); + } + + @BeforeMethod(alwaysRun=true) + public void setup() { + restClient.configureRequestSpec() + .setBasePath("") + .addHeader("Content-Type", "application/json"); + } + + @Test + public void userShouldNotCreateAMailForm() { + restClient.authenticateUser(testUser); + + String body = generateBody(createMailParameters(adminUser, testUser)); + String endpoint = String.format(ACTION_FORM_PROCESSOR_ENDPOINT, MAIL_ACTION); + + RestRequest request = RestRequest.requestWithBody(HttpMethod.POST, body, endpoint); + RestResponse response = restClient.process(request); + + response.assertThat().statusCode(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .assertThat().body(ERROR_MESSAGE_FIELD, containsString(ERROR_MESSAGE_ACCESS_RESTRICTED)); + } + + @Test + public void adminShouldCreateAMailForm() { + restClient.authenticateUser(adminUser); + + String body = generateBody(createMailParameters(adminUser, testUser)); + String endpoint = String.format(ACTION_FORM_PROCESSOR_ENDPOINT, MAIL_ACTION); + + RestRequest request = RestRequest.requestWithBody(HttpMethod.POST, body, endpoint); + RestResponse response = restClient.process(request); + + response.assertThat().statusCode(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .assertThat().body(ERROR_MESSAGE_FIELD, containsString(getExpectedEmailSendFailureMessage(testUser))); + } + + private String generateBody(Map mailParameters) { + JSONObject json = new JSONObject(); + mailParameters.forEach((key, value) -> json.put(PROPERTY_PREFIX + key, value)); + + return json.toJSONString(); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/RuleAdminAccessRestrictionTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/RuleAdminAccessRestrictionTest.java new file mode 100644 index 0000000000..8ec6ffbda4 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/RuleAdminAccessRestrictionTest.java @@ -0,0 +1,102 @@ +package org.alfresco.rest.actions.access; + +import org.alfresco.rest.RestTest; +import org.alfresco.rest.actions.access.pojo.Rule; +import org.alfresco.rest.core.RestRequest; +import org.alfresco.rest.core.RestResponse; +import org.alfresco.rest.core.RestWrapper; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.UserModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.ERROR_MESSAGE_ACCESS_RESTRICTED; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.ERROR_MESSAGE_FIELD; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.MAIL_ACTION; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.createMailParameters; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.createRuleWithAction; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.mapObjectToJSON; +import static org.hamcrest.Matchers.containsString; + +public class RuleAdminAccessRestrictionTest extends RestTest { + + private static final String CREATE_RULE_ENDPOINT = "alfresco/service/api/node/workspace/SpacesStore/%s/ruleset/rules"; + + private UserModel adminUser; + private UserModel testUser; + private FolderModel testFolder; + + @Autowired + protected RestWrapper restClient; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception { + adminUser = dataUser.getAdminUser(); + + testUser = dataUser.createRandomTestUser(); + testSite = dataSite.usingUser(testUser) + .createPublicRandomSite(); + testFolder = dataContent.usingUser(testUser) + .usingSite(testSite) + .createFolder(); + } + + @BeforeMethod(alwaysRun=true) + public void setup() { + restClient.configureRequestSpec().setBasePath(""); + } + + @Test + public void userShouldNotBeAbleToCreateANewRule() { + restClient.authenticateUser(testUser); + + Rule rule = createRuleWithAction(MAIL_ACTION, createMailParameters(adminUser, testUser)); + String ruleRequestBody = mapObjectToJSON(rule); + String ruleEndpoint = String.format(CREATE_RULE_ENDPOINT, testFolder.getNodeRef()); + + RestRequest request = RestRequest.requestWithBody(HttpMethod.POST, ruleRequestBody, ruleEndpoint); + RestResponse response = restClient.process(request); + + response.assertThat().statusCode(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .assertThat().body(ERROR_MESSAGE_FIELD, containsString(ERROR_MESSAGE_ACCESS_RESTRICTED)); + } + + @Test + public void adminShouldBeAbleToCreateANewRule() { + restClient.authenticateUser(adminUser); + + Rule rule = createRuleWithAction(MAIL_ACTION, createMailParameters(adminUser, testUser)); + String ruleRequestBody = mapObjectToJSON(rule); + String ruleEndpoint = String.format(CREATE_RULE_ENDPOINT, testFolder.getNodeRef()); + + RestRequest request = RestRequest.requestWithBody(HttpMethod.POST, ruleRequestBody, ruleEndpoint); + RestResponse response = restClient.process(request); + + response.assertThat().statusCode(HttpStatus.OK.value()); + } + + @Test + public void userShouldAddAFileToFolderWithMailRule() { + restClient.authenticateUser(adminUser); + + Rule rule = createRuleWithAction(MAIL_ACTION, createMailParameters(adminUser, testUser)); + String ruleRequestBody = mapObjectToJSON(rule); + String ruleEndpoint = String.format(CREATE_RULE_ENDPOINT, testFolder.getNodeRef()); + + RestRequest request = RestRequest.requestWithBody(HttpMethod.POST, ruleRequestBody, ruleEndpoint); + RestResponse response = restClient.process(request); + + response.assertThat().statusCode(HttpStatus.OK.value()); + + dataContent.usingUser(testUser) + .usingSite(testSite) + .usingResource(testFolder) + .createContent(FileModel.getRandomFileModel(FileType.TEXT_PLAIN)); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/V0AdminAccessRestrictionTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/V0AdminAccessRestrictionTest.java new file mode 100644 index 0000000000..ffabba2a36 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/V0AdminAccessRestrictionTest.java @@ -0,0 +1,76 @@ +package org.alfresco.rest.actions.access; + +import org.alfresco.rest.actions.access.pojo.Action; +import org.alfresco.rest.RestTest; +import org.alfresco.rest.core.RestRequest; +import org.alfresco.rest.core.RestResponse; +import org.alfresco.rest.core.RestWrapper; +import org.alfresco.utility.model.UserModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.ERROR_MESSAGE_ACCESS_RESTRICTED; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.ERROR_MESSAGE_FIELD; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.MAIL_ACTION; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.createActionWithParameters; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.createMailParameters; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.getExpectedEmailSendFailureMessage; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.mapObjectToJSON; +import static org.hamcrest.Matchers.containsString; + +public class V0AdminAccessRestrictionTest extends RestTest { + + private static final String ACTION_QUEUE_ENDPOINT = "alfresco/service/api/actionQueue?async=false"; + + private UserModel adminUser; + private UserModel testUser; + + @Autowired + protected RestWrapper restClient; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception { + adminUser = dataUser.getAdminUser(); + + testUser = dataUser.createRandomTestUser(); + testSite = dataSite.usingUser(testUser) + .createPublicRandomSite(); + } + + @BeforeMethod(alwaysRun=true) + public void setup() { + restClient.configureRequestSpec().setBasePath(""); + } + + @Test + public void userShouldNotExecuteMailActionQueue() { + restClient.authenticateUser(testUser); + + Action action = createActionWithParameters(MAIL_ACTION, createMailParameters(adminUser, testUser)); + String actionRequestBody = mapObjectToJSON(action); + + RestRequest request = RestRequest.requestWithBody(HttpMethod.POST, actionRequestBody, ACTION_QUEUE_ENDPOINT); + RestResponse response = restClient.process(request); + + response.assertThat().statusCode(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .assertThat().body(ERROR_MESSAGE_FIELD, containsString(ERROR_MESSAGE_ACCESS_RESTRICTED)); + } + + @Test + public void adminShouldExecuteMailActionQueue() { + restClient.authenticateUser(adminUser); + + Action action = createActionWithParameters(MAIL_ACTION, createMailParameters(adminUser, testUser)); + String actionRequestBody = mapObjectToJSON(action); + + RestRequest request = RestRequest.requestWithBody(HttpMethod.POST, actionRequestBody, ACTION_QUEUE_ENDPOINT); + RestResponse response = restClient.process(request); + + response.assertThat().statusCode(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .assertThat().body(ERROR_MESSAGE_FIELD, containsString(getExpectedEmailSendFailureMessage(testUser))); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/V1AdminAccessRestrictionTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/V1AdminAccessRestrictionTest.java new file mode 100644 index 0000000000..680c8b3bc7 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/V1AdminAccessRestrictionTest.java @@ -0,0 +1,63 @@ +package org.alfresco.rest.actions.access; + +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.ERROR_MESSAGE_ACCESS_RESTRICTED; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.MAIL_ACTION; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.createMailParameters; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import org.alfresco.rest.RestTest; +import org.alfresco.rest.core.RestWrapper; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.UserModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class V1AdminAccessRestrictionTest extends RestTest { + + private UserModel adminUser; + private UserModel testUser; + private FolderModel testFolder; + + @Autowired + protected RestWrapper restClient; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception { + adminUser = dataUser.getAdminUser(); + + testUser = dataUser.createRandomTestUser(); + testSite = dataSite.usingUser(testUser) + .createPublicRandomSite(); + testFolder = dataContent.usingUser(testUser) + .usingSite(testSite) + .createFolder(); + } + + @Test + public void userShouldNotExecuteMailAction() throws Exception { + restClient.authenticateUser(testUser) + .withCoreAPI() + .usingActions() + .executeAction(MAIL_ACTION, testFolder, createMailParameters(adminUser, testUser)); + + restClient.onResponse() + .assertThat().statusCode(HttpStatus.FORBIDDEN.value()) + .assertThat().body("entry.id", nullValue()); + restClient.assertLastError().containsSummary(ERROR_MESSAGE_ACCESS_RESTRICTED); + } + + @Test + public void adminShouldExecuteMailAction() throws Exception { + restClient.authenticateUser(adminUser) + .withCoreAPI() + .usingActions() + .executeAction(MAIL_ACTION, testFolder, createMailParameters(adminUser, testUser)); + + restClient.onResponse() + .assertThat().statusCode(HttpStatus.ACCEPTED.value()) + .assertThat().body("entry.id", notNullValue()); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/pojo/Action.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/pojo/Action.java new file mode 100644 index 0000000000..b8d31f4852 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/pojo/Action.java @@ -0,0 +1,58 @@ +package org.alfresco.rest.actions.access.pojo; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +public class Action { + private String actionDefinitionName; + private String actionedUponNode; + private List conditions; + private List actions; + private Map parameterValues; + + private boolean executeAsynchronously; + + public void setExecuteAsynchronously(boolean executeAsynchronously) { + this.executeAsynchronously = executeAsynchronously; + } + public String getActionDefinitionName() { + return actionDefinitionName; + } + + public void setActionDefinitionName(String actionDefinitionName) { + this.actionDefinitionName = actionDefinitionName; + } + + public String getActionedUponNode() { + return actionedUponNode; + } + + public void setActionedUponNode(String actionedUponNode) { + this.actionedUponNode = actionedUponNode; + } + + public List getConditions() { + return conditions; + } + + public void setConditions(List conditions) { + this.conditions = conditions; + } + + public List getActions() { + return actions; + } + + public void setActions(List actions) { + this.actions = actions; + } + + public Map getParameterValues() { + return parameterValues; + } + + public void setParameterValues(Map parameterValues) { + this.parameterValues = parameterValues; + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/pojo/ActionCondition.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/pojo/ActionCondition.java new file mode 100644 index 0000000000..9514169965 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/pojo/ActionCondition.java @@ -0,0 +1,24 @@ +package org.alfresco.rest.actions.access.pojo; + +import java.util.Map; + +public class ActionCondition { + private String conditionDefinitionName; + private Map parameterValues; + + public String getConditionDefinitionName() { + return conditionDefinitionName; + } + + public void setConditionDefinitionName(String conditionDefinitionName) { + this.conditionDefinitionName = conditionDefinitionName; + } + + public Map getParameterValues() { + return parameterValues; + } + + public void setParameterValues(Map parameterValues) { + this.parameterValues = parameterValues; + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/pojo/Rule.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/pojo/Rule.java new file mode 100644 index 0000000000..2b47748cc5 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/actions/access/pojo/Rule.java @@ -0,0 +1,78 @@ +package org.alfresco.rest.actions.access.pojo; + +import java.util.List; + +public class Rule { + private String id; + private String title; + private String description; + private List ruleType; + private boolean executeAsynchronously; + private boolean disabled; + private boolean applyToChildren; + private Action action; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getRuleType() { + return ruleType; + } + + public void setRuleType(List ruleType) { + this.ruleType = ruleType; + } + + public boolean isExecuteAsynchronously() { + return executeAsynchronously; + } + + public void setExecuteAsynchronously(boolean executeAsynchronously) { + this.executeAsynchronously = executeAsynchronously; + } + + public boolean isDisabled() { + return disabled; + } + + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + public boolean isApplyToChildren() { + return applyToChildren; + } + + public void setApplyToChildren(boolean applyToChildren) { + this.applyToChildren = applyToChildren; + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/CreateRulesTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/CreateRulesTests.java new file mode 100644 index 0000000000..68605676b5 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/CreateRulesTests.java @@ -0,0 +1,965 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rules; + +import static java.util.stream.Collectors.toList; + +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.ERROR_MESSAGE_ACCESS_RESTRICTED; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.MAIL_ACTION; +import static org.alfresco.rest.rules.RulesTestsUtils.CHECKIN_ACTION; +import static org.alfresco.rest.rules.RulesTestsUtils.COPY_ACTION; +import static org.alfresco.rest.rules.RulesTestsUtils.ID; +import static org.alfresco.rest.rules.RulesTestsUtils.INVERTED; +import static org.alfresco.rest.rules.RulesTestsUtils.IS_SHARED; +import static org.alfresco.rest.rules.RulesTestsUtils.RULE_NAME_DEFAULT; +import static org.alfresco.rest.rules.RulesTestsUtils.RULE_SCRIPT_PARAM_ID; +import static org.alfresco.rest.rules.RulesTestsUtils.SCRIPT_ACTION; +import static org.alfresco.rest.rules.RulesTestsUtils.TEMPLATE_PARAM; +import static org.alfresco.utility.constants.UserRole.SiteCollaborator; +import static org.alfresco.utility.constants.UserRole.SiteConsumer; +import static org.alfresco.utility.constants.UserRole.SiteContributor; +import static org.alfresco.utility.constants.UserRole.SiteManager; +import static org.alfresco.utility.model.FileModel.getRandomFileModel; +import static org.alfresco.utility.model.FileType.TEXT_PLAIN; +import static org.alfresco.utility.model.UserModel.getRandomUserModel; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.junit.Assert.assertEquals; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +import javax.json.Json; +import javax.json.JsonObject; + +import org.alfresco.rest.RestTest; +import org.alfresco.rest.model.RestActionBodyExecTemplateModel; +import org.alfresco.rest.model.RestActionConstraintModel; +import org.alfresco.rest.model.RestCompositeConditionDefinitionModel; +import org.alfresco.rest.model.RestRuleModel; +import org.alfresco.rest.model.RestRuleModelsCollection; +import org.alfresco.utility.constants.UserRole; +import org.alfresco.utility.model.ContentModel; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.apache.chemistry.opencmis.client.api.CmisObject; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Tests for POST /nodes/{nodeId}/rule-sets/{ruleSetId}/rules. + */ +@Test(groups = {TestGroup.RULES}) +public class CreateRulesTests extends RestTest +{ + private UserModel user; + private SiteModel site; + private FolderModel ruleFolder; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + user = dataUser.createRandomTestUser(); + site = dataSite.usingUser(user).createPublicRandomSite(); + ruleFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + } + + /** + * Check we can create a rule. + *

+ * Also check that the isShared field is not returned when not requested. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void createRule() + { + RestRuleModel ruleModel = rulesUtils.createRuleModelWithModifiedValues(); + + RestRuleModel rule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + RestRuleModel expectedRuleModel = rulesUtils.createRuleModelWithModifiedValues(); + restClient.assertStatusCodeIs(CREATED); + rule.assertThat().isEqualTo(expectedRuleModel, ID, IS_SHARED) + .assertThat().field(ID).isNotNull() + .assertThat().field(IS_SHARED).isNull(); + } + + /** Check creating a rule in a non-existent folder returns an error. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void createRuleInNonExistentFolder() + { + STEP("Try to create a rule in non-existent folder."); + FolderModel nonExistentFolder = FolderModel.getRandomFolderModel(); + nonExistentFolder.setNodeRef("fake-id"); + + RestRuleModel ruleModel = new RestRuleModel(); + ruleModel.setName("ruleName"); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(nonExistentFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(NOT_FOUND); + restClient.assertLastError().containsSummary("Folder with id fake-id was not found"); + } + + /** Check creating a rule in a non-existent rule set returns an error. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void createRuleInNonExistentRuleSet() + { + STEP("Try to create a rule in non-existent rule set."); + RestRuleModel ruleModel = new RestRuleModel(); + ruleModel.setName("ruleName"); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingRuleSet("fake-id").createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(NOT_FOUND); + restClient.assertLastError().containsSummary("Rule set with id fake-id was not found"); + } + + /** Try to create a rule without a name and check the error. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void createRuleWithEmptyName() + { + RestRuleModel ruleModel = rulesUtils.createRuleModel(""); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary("Rule name is a mandatory parameter"); + } + + /** Check we can create two rules with the same name. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void duplicateRuleNameIsAcceptable() + { + RestRuleModel ruleModel = rulesUtils.createRuleModel("duplicateRuleName"); + + STEP("Create two identical rules"); + RestRuleModel ruleA = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + RestRuleModel ruleB = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + + // Check that the names are the same but the ids are different. + ruleA.assertThat().field("name").is(ruleB.getName()); + ruleA.assertThat().field("id").isNot(ruleB.getId()); + } + + /** Check that a user without permission to view the folder cannot create a rule in it. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void requireReadPermissionToCreateRule() + { + STEP("Create a user and use them to create a private site containing a folder"); + UserModel privateUser = dataUser.createRandomTestUser(); + SiteModel privateSite = dataSite.usingUser(privateUser).createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingUser(privateUser).usingSite(privateSite).createFolder(); + + STEP("Try to use a different user to create a rule in the private folder"); + RestRuleModel ruleModel = new RestRuleModel(); + ruleModel.setName("ruleName"); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(FORBIDDEN); + restClient.assertLastError().containsSummary("Insufficient permissions to manage rules"); + } + + /** Check that a Collaborator cannot create a rule in a folder in a private site. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void siteCollaboratorCannotCreateRule() + { + testRolePermissionsWith(SiteCollaborator); + + restClient.assertStatusCodeIs(FORBIDDEN); + restClient.assertLastError().containsSummary("Insufficient permissions to manage rules"); + } + + /** Check that a Contributor cannot create a rule in a private folder. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void siteContributorCannotCreateRule() + { + testRolePermissionsWith(SiteContributor); + + restClient.assertStatusCodeIs(FORBIDDEN); + restClient.assertLastError().containsSummary("Insufficient permissions to manage rules"); + } + + /** Check that a Consumer cannot create a rule in a folder in a private site. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void siteConsumerCannotCreateRule() + { + testRolePermissionsWith(SiteConsumer); + + restClient.assertStatusCodeIs(FORBIDDEN); + restClient.assertLastError().containsSummary("Insufficient permissions to manage rules"); + } + + /** Check that a siteManager can create a rule in a folder in a private site. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void siteManagerCanCreateRule() + { + testRolePermissionsWith(SiteManager) + .assertThat().field("id").isNotNull() + .assertThat().field("name").is("testRule"); + restClient.assertStatusCodeIs(CREATED); + } + + /** Check we can't create a rule under a document node. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void tryToCreateRuleUnderDocument() + { + STEP("Create a document."); + FileModel fileModel = dataContent.usingUser(user).usingSite(site).createContent(getRandomFileModel(TEXT_PLAIN)); + + RestRuleModel ruleModel = new RestRuleModel(); + ruleModel.setName("ruleName"); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(fileModel).usingDefaultRuleSet().createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary("folder is expected"); + } + + /** Check we can create several rules. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void createRules() + { + STEP("Create a list of rules in one POST request"); + List ruleNames = List.of("ruleA", "ruleB", "ruleC"); + List ruleModels = ruleNames.stream().map(rulesUtils::createRuleModel).collect(toList()); + + RestRuleModelsCollection rules = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createListOfRules(ruleModels); + + restClient.assertStatusCodeIs(CREATED); + + assertEquals("Unexpected number of rules received in response.", ruleNames.size(), rules.getEntries().size()); + IntStream.range(0, ruleModels.size()).forEach(i -> + rules.getEntries().get(i).onModel() + .assertThat().field("id").isNotNull() + .assertThat().field("name").is(ruleNames.get(i))); + } + + /** Try to create several rules with an error in one of them. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void createRulesWithOneError() + { + STEP("Try to create a three rules but the middle one has an error."); + RestRuleModel ruleA = rulesUtils.createRuleModel("ruleA"); + RestRuleModel ruleB = rulesUtils.createRuleModel(""); + // Don't set a name for Rule B. + RestRuleModel ruleC = rulesUtils.createRuleModel("ruleC"); + List ruleModels = List.of(ruleA, ruleB, ruleC); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet().createListOfRules(ruleModels); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary("Rule name is a mandatory parameter"); + } + + /** Check we can create a rule without description. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void createRuleWithoutDescription() + { + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + UserModel admin = dataUser.getAdminUser(); + + RestRuleModel rule = restClient.authenticateUser(admin).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(CREATED); + rule.assertThat().field("id").isNotNull() + .assertThat().field("name").is(RULE_NAME_DEFAULT) + .assertThat().field("description").isNull(); + } + + /** Check we can create a rule without specifying triggers but with the default "inbound" value. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void createRuleWithoutTriggers() + { + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + UserModel admin = dataUser.getAdminUser(); + + RestRuleModel rule = restClient.authenticateUser(admin).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(CREATED); + rule.assertThat().field("id").isNotNull() + .assertThat().field("name").is(RULE_NAME_DEFAULT) + .assertThat().field("triggers").is(List.of("inbound")); + } + + /** Check we can create a rule without error script. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void createRuleWithoutErrorScript() + { + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + UserModel admin = dataUser.getAdminUser(); + + RestRuleModel rule = restClient.authenticateUser(admin).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(CREATED); + rule.assertThat().field("id").isNotNull() + .assertThat().field("name").is(RULE_NAME_DEFAULT) + .assertThat().field("errorScript").isNull(); + } + + /** Check we can create a rule with irrelevant isShared flag, and it doesn't have impact to the process. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void createRuleWithSharedFlag() + { + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + ruleModel.setIsShared(true); + UserModel admin = dataUser.getAdminUser(); + + RestRuleModel rule = restClient.authenticateUser(admin).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(CREATED); + rule.assertThat().field("id").isNotNull() + .assertThat().field("name").is(RULE_NAME_DEFAULT) + .assertThat().field("isShared").isNull(); + } + + /** Check we can create a rule. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void createRuleAndIncludeFieldsInResponse() + { + RestRuleModel ruleModel = rulesUtils.createRuleModel("ruleName"); + + RestRuleModel rule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .include("isShared") + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(CREATED); + rule.assertThat().field("isShared").isNotNull(); + } + + private RestRuleModel testRolePermissionsWith(UserRole userRole) + { + STEP("Create a user and use them to create a private site containing a folder"); + SiteModel privateSite = dataSite.usingUser(user).createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingUser(user).usingSite(privateSite).createFolder(); + + STEP(String.format("Add a user with '%s' role in the private site's folder", userRole.toString())); + UserModel userWithRole = dataUser.createRandomTestUser(); + dataUser.addUserToSite(userWithRole, privateSite, userRole); + RestRuleModel ruleModel = rulesUtils.createRuleModel("testRule", List.of(rulesUtils.createAddAudioAspectAction())); + + return restClient.authenticateUser(userWithRole).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + } + + /** Check that the folder's owner can create rules, even if it is in a private site they aren't a member of. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void checkOwnerCanCreateRule() + { + STEP("Use admin to create a private site."); + SiteModel privateSite = dataSite.usingUser(dataUser.getAdminUser()).createPrivateRandomSite(); + + STEP("Add the user to the site, let them create a folder and then evict them from the site again."); + dataUser.addUserToSite(user, privateSite, SiteManager); + FolderModel folder = dataContent.usingUser(user).usingSite(privateSite).createFolder(); + dataUser.removeUserFromSite(user, privateSite); + + STEP("Check the folder owner can create a rule."); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).usingDefaultRuleSet().createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(CREATED); + } + + /** Check that an administrator can create a rule in a private site even if they aren't a member. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void checkAdminCanCreateRule() + { + STEP("Use a user to create a private site with a folder."); + SiteModel privateSite = dataSite.usingUser(user).createPrivateRandomSite(); + FolderModel folder = dataContent.usingUser(user).usingSite(privateSite).createFolder(); + + STEP("Check admin can create a rule."); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + restClient.authenticateUser(dataUser.getAdminUser()).withPrivateAPI().usingNode(folder).usingDefaultRuleSet().createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(CREATED); + } + + /** Check that a coordinator can create rules in folders outside sites. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void checkCoordinatorCanCreateRule() + { + STEP("Create a folder in the user's file space."); + FolderModel folder = dataContent.usingUser(user).usingUserHome().createFolder(); + + STEP("Create another user as a coordinator for this folder."); + UserModel coordinator = dataUser.createRandomTestUser("Rules"); + /* + Update folder node properties to add a coordinator + { "permissions": { "isInheritanceEnabled": true, "locallySet": { "authorityId": "coordinator.getUsername()", + "name": "Coordinator", "accessStatus":"ALLOWED" } } } + */ + String putBody = getAddPermissionsBody(coordinator.getUsername(), "Coordinator"); + restClient.authenticateUser(user).withCoreAPI().usingNode(folder).updateNode(putBody); + + STEP("Check the coordinator can create a rule."); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + restClient.authenticateUser(coordinator).withPrivateAPI().usingNode(folder).usingDefaultRuleSet().createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(CREATED); + } + + /** Check that an editor cannot create rules in folders outside sites. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void checkEditorCannotCreateRule() + { + STEP("Create a folder in the user's file space."); + FolderModel folder = dataContent.usingUser(user).usingUserHome().createFolder(); + + STEP("Create another user as a editor for this folder."); + UserModel editor = dataUser.createRandomTestUser(); + /* + Update folder node properties to add an editor + { "permissions": { "isInheritanceEnabled": true, "locallySet": { "authorityId": "editor.getUsername()", + "name": "Coordinator", "accessStatus":"ALLOWED" } } } + */ + String putBody = getAddPermissionsBody(editor.getUsername(), "Editor"); + restClient.authenticateUser(user).withCoreAPI().usingNode(folder).updateNode(putBody); + + STEP("Check the editor can create a rule."); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + restClient.authenticateUser(editor).withPrivateAPI().usingNode(folder).usingDefaultRuleSet().createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(FORBIDDEN); + } + + /** Check that a collaborator cannot create rules in folders outside sites. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void checkCollaboratorCannotCreateRule() + { + STEP("Create a folder in the user's file space."); + FolderModel folder = dataContent.usingUser(user).usingUserHome().createFolder(); + + STEP("Create another user as a collaborator for this folder."); + UserModel collaborator = dataUser.createRandomTestUser(); + /* + Update folder node properties to add a collaborator + { "permissions": { "isInheritanceEnabled": true, "locallySet": { "authorityId": "collaborator.getUsername()", + "name": "Coordinator", "accessStatus":"ALLOWED" } } } + */ + String putBody = getAddPermissionsBody(collaborator.getUsername(), "Collaborator"); + restClient.authenticateUser(user).withCoreAPI().usingNode(folder).updateNode(putBody); + + STEP("Check the collaborator can create a rule."); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + restClient.authenticateUser(collaborator).withPrivateAPI().usingNode(folder).usingDefaultRuleSet().createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(FORBIDDEN); + } + + /** + * Check we can create a rule with several actions. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithActions() + { + final UserModel admin = dataUser.getAdminUser(); + + final RestRuleModel rule = restClient.authenticateUser(admin).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(rulesUtils.createRuleWithVariousActions()); + + RestRuleModel expectedRuleModel = rulesUtils.createRuleModelWithDefaultValues(); + expectedRuleModel.setActions(rulesUtils.createRuleWithVariousActions().getActions()); + expectedRuleModel.setTriggers(List.of("inbound")); + + restClient.assertStatusCodeIs(CREATED); + rule.assertThat().isEqualTo(expectedRuleModel, ID, IS_SHARED) + .assertThat().field(IS_SHARED).isNull(); + } + + /** + * Check get an error when creating a rule with action with empty parameter value. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithEmptyActionParameterValueShouldFail() + { + final RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + final RestActionBodyExecTemplateModel checkinAction = rulesUtils.createCustomActionModel(CHECKIN_ACTION, Map.of("description", "")); + ruleModel.setActions(List.of(checkinAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(BAD_REQUEST).assertLastError().containsSummary("Action parameter should not have empty or null value"); + } + + /** + * Check can create a rule with action without any parameters when action definition states all of them are optional. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithoutParameterWhenTheyAreOptional() + { + final RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + final RestActionBodyExecTemplateModel checkinAction = rulesUtils.createCustomActionModel(CHECKIN_ACTION, null); + ruleModel.setActions(List.of(checkinAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(CREATED); + } + + /** Check that a normal user cannot create rules that use private actions. */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithActions_userCannotUsePrivateAction() + { + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(rulesUtils.createRuleWithPrivateAction()); + + restClient.assertStatusCodeIs(FORBIDDEN) + .assertLastError().containsSummary(ERROR_MESSAGE_ACCESS_RESTRICTED); + } + + /** Check that an administrator can create rules that use private actions. */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithActions_adminCanUsePrivateAction() + { + restClient.authenticateUser(dataUser.getAdminUser()).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(rulesUtils.createRuleWithPrivateAction()); + + restClient.assertStatusCodeIs(CREATED); + } + + /** + * Check that an administrator can create rules with email (private) action with reference to an email template. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithActions_adminCanUseMailActionWithTemplate() + { + final RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + final RestActionBodyExecTemplateModel mailAction = new RestActionBodyExecTemplateModel(); + mailAction.setActionDefinitionId(MAIL_ACTION); + final Map params = new HashMap<>(); + final UserModel sender = getRandomUserModel(); + final UserModel recipient = getRandomUserModel(); + params.put("from", sender.getEmailAddress()); + params.put("to", recipient.getEmailAddress()); + params.put("subject", "Test"); + final RestActionConstraintModel constraint = rulesUtils.getConstraintsForActionParam(user, MAIL_ACTION, TEMPLATE_PARAM); + String templateScriptRef = constraint.getConstraintValues().stream().findFirst().get().getValue(); + params.put(TEMPLATE_PARAM, templateScriptRef); + mailAction.setParams(params); + ruleModel.setActions(List.of(mailAction)); + + restClient.authenticateUser(dataUser.getAdminUser()).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(CREATED); + } + + /** + * Check we get error when attempt to create a rule without any actions. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithoutActionsShouldFail() + { + final RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + ruleModel.setActions(null); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary("A rule must have at least one action"); + } + + /** + * Check we get error when attempt to create a rule with invalid action. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithInvalidActionsShouldFail() + { + final RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + final String actionDefinitionId = "invalid-definition-value"; + final RestActionBodyExecTemplateModel invalidAction = rulesUtils.createCustomActionModel(actionDefinitionId, Map.of("dummy-key", "dummy-value")); + ruleModel.setActions(List.of(invalidAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary(String.format("Invalid rule action definition requested %s", actionDefinitionId)); + } + + /** + * Check we get error when attempt to create a rule with an action tha is not applicable to rules. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithNotApplicableActionShouldFail() + { + final RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + final RestActionBodyExecTemplateModel invalidAction = + rulesUtils.createCustomActionModel(RulesTestsUtils.DELETE_RENDITION_ACTION, Map.of("dummy-key", "dummy-value")); + ruleModel.setActions(List.of(invalidAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary(String.format("Invalid rule action definition requested %s", RulesTestsUtils.DELETE_RENDITION_ACTION)); + } + + /** + * Check we get error when attempt to create a rule with missing action parameters. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithMissingActionParametersShouldFail() + { + final RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + final RestActionBodyExecTemplateModel invalidAction = + rulesUtils.createCustomActionModel(RulesTestsUtils.COPY_ACTION, Collections.emptyMap()); + ruleModel.setActions(List.of(invalidAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary( + String.format("Action parameters should not be null or empty for this action. See Action Definition for action of: %s", + COPY_ACTION)); + } + + /** + * Check we get error when attempt to create a rule with parameter not fulfilling constraint. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithActionParameterNotFulfillingConstraint() + { + final RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + final String actionDefinitionId = SCRIPT_ACTION; + final String scriptRef = RULE_SCRIPT_PARAM_ID; + final String scriptNodeId = "dummy-script-node-id"; + final RestActionBodyExecTemplateModel scriptAction = rulesUtils.createCustomActionModel(actionDefinitionId, Map.of(scriptRef, scriptNodeId)); + ruleModel.setActions(List.of(scriptAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(BAD_REQUEST); + final String acScriptsConstraint = "ac-scripts"; + restClient.assertLastError().containsSummary( + String.format("Action parameter: %s has invalid value (%s). Look up possible values for constraint name %s", + scriptRef, scriptNodeId, acScriptsConstraint)); + } + + /** + * Check we get error when attempt to create a rule with action parameter that should not be passed. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithInvalidActionParameterShouldFail() + { + final RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + final String invalidParameterKey = "invalidParameterKey"; + final RestActionBodyExecTemplateModel invalidAction = rulesUtils.createCustomActionModel( + RulesTestsUtils.ADD_FEATURES_ACTION, Map.of(invalidParameterKey, "dummyValue")); + ruleModel.setActions(List.of(invalidAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary( + String.format("Action of definition id: %s must not contain parameter of name: %s", RulesTestsUtils.ADD_FEATURES_ACTION, invalidParameterKey)); + } + + /** + * Check we get error when attempt to create a rule with missing mandatory action parameter. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithoutMandatoryActionParametersShouldFail() + { + final RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + final RestActionBodyExecTemplateModel invalidAction = rulesUtils.createCustomActionModel(COPY_ACTION, Map.of("deep-copy",false)); + ruleModel.setActions(List.of(invalidAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary("Missing action's mandatory parameter: destination-folder"); + } + + /** + * Check we get error when attempting to create a rule that copies files to a non-existent folder. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void createRuleThatUsesNonExistentNode() + { + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + RestActionBodyExecTemplateModel invalidAction = rulesUtils.createCustomActionModel( + COPY_ACTION, Map.of("destination-folder", "non-existent-node")); + ruleModel.setActions(List.of(invalidAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(NOT_FOUND); + restClient.assertLastError().containsSummary("The entity with id: non-existent-node was not found"); + } + + /** + * Check we get error when attempting to create a rule that references a folder that the user does not have read permission for. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void createRuleThatUsesNodeWithoutReadPermission() + { + SiteModel privateSite = dataSite.usingAdmin().createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingAdmin().usingSite(privateSite).createFolder(); + + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + RestActionBodyExecTemplateModel invalidAction = rulesUtils.createCustomActionModel( + COPY_ACTION, Map.of("destination-folder", privateFolder.getNodeRef())); + ruleModel.setActions(List.of(invalidAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(NOT_FOUND); + restClient.assertLastError().containsSummary("The entity with id: " + privateFolder.getNodeRef() + " was not found"); + } + + /** + * Check we get error when attempting to create a rule that copies files to a folder that a user only has read permission for. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void createRuleThatWritesToNodeWithoutPermission() + { + SiteModel privateSite = dataSite.usingAdmin().createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingAdmin().usingSite(privateSite).createFolder(); + dataUser.usingAdmin().addUserToSite(user, privateSite, SiteConsumer); + + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + RestActionBodyExecTemplateModel invalidAction = rulesUtils.createCustomActionModel( + COPY_ACTION, Map.of("destination-folder", privateFolder.getNodeRef())); + + ruleModel.setActions(List.of(invalidAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(FORBIDDEN); + restClient.assertLastError().containsSummary("No proper permissions for node: " + privateFolder.getNodeRef()); + } + + /** + * Check we get error when attempting to create a rule that moves files to a node which is not a folder + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleThatMovesToNodeWhichIsNotAFolderShouldFail() + { + final FileModel fileModel = dataContent.usingUser(user).usingSite(site).createContent(getRandomFileModel(TEXT_PLAIN)); + + final RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + final RestActionBodyExecTemplateModel invalidAction = rulesUtils.createCustomActionModel( + RulesTestsUtils.MOVE_ACTION, Map.of("destination-folder", fileModel.getNodeRef())); + ruleModel.setActions(List.of(invalidAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary("Node is not a folder " + fileModel.getNodeRef()); + } + + + /** + * Check we get error when attempting to create a rule with mail action defined with non-existing mail template. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithMailActionReferringToNonExistingTemplate() + { + final RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + final RestActionBodyExecTemplateModel mailAction = new RestActionBodyExecTemplateModel(); + mailAction.setActionDefinitionId(MAIL_ACTION); + final Map params = new HashMap<>(); + final UserModel sender = getRandomUserModel(); + final UserModel recipient = getRandomUserModel(); + params.put("from", sender.getEmailAddress()); + params.put("to", recipient.getEmailAddress()); + params.put("subject", "Test"); + final String mailTemplate = "non-existing-node-id"; + params.put(TEMPLATE_PARAM, mailTemplate); + mailAction.setParams(params); + ruleModel.setActions(List.of(mailAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary("Action parameter: template has invalid value (" + mailTemplate + + "). Look up possible values for constraint name ac-email-templates"); + } + + /** + * Check the user can create a rule with a script. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void checkCanUseScriptInRule() + { + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + RestActionBodyExecTemplateModel scriptAction = rulesUtils.createCustomActionModel( + SCRIPT_ACTION, Map.of(RULE_SCRIPT_PARAM_ID, rulesUtils.getReviewAndApproveWorkflowNode())); + ruleModel.setActions(List.of(scriptAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(CREATED); + } + + /** + * Check the script has to be stored in the scripts directory in the data dictionary. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void checkCantUseNodeOutsideScriptsDirectory() + { + STEP("Copy script to location outside data dictionary."); + FolderModel folderOutsideDataDictionary = dataContent.usingUser(user).usingSite(site).createFolder(); + String sourceNodeId = rulesUtils.getReviewAndApproveWorkflowNode(); + ContentModel sourceNode = new ContentModel("/Data Dictionary/Scripts/start-pooled-review-workflow.js"); + sourceNode.setNodeRef("/workspace://SpacesStore/" + sourceNodeId); + CmisObject scriptOutsideDataDictionary = dataContent.getContentActions().copyTo(dataUser.getAdminUser().getUsername(), + dataUser.getAdminUser().getPassword(), + sourceNode.getCmisLocation(), + folderOutsideDataDictionary.getCmisLocation()); + String scriptId = scriptOutsideDataDictionary.getId().substring(0, scriptOutsideDataDictionary.getId().indexOf(";")); + + STEP("Try to use this script in rule."); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + RestActionBodyExecTemplateModel scriptAction = rulesUtils.createCustomActionModel( + SCRIPT_ACTION, Map.of(RULE_SCRIPT_PARAM_ID, scriptId)); + ruleModel.setActions(List.of(scriptAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(BAD_REQUEST) + .assertLastError().containsSummary("script-ref has invalid value"); + } + + /** + * Check a real category needs to be supplied when linking to a category. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void checkLinkToCategoryNeedsRealCategory() + { + STEP("Attempt to link to a category with a folder node, rather than a category node."); + String nonCategoryNodeRef = ruleFolder.getNodeRef(); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + RestActionBodyExecTemplateModel categoryAction = rulesUtils.createCustomActionModel( + RulesTestsUtils.LINK_CATEGORY_ACTION, Map.of("category-value", nonCategoryNodeRef)); + ruleModel.setActions(List.of(categoryAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(BAD_REQUEST); + } + + /** + * Check we can create a rule with multiple conditions + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithConditions() + { + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + ruleModel.setConditions(rulesUtils.createVariousConditions()); + + RestRuleModel rule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + RestRuleModel expectedRuleModel = rulesUtils.createRuleModelWithDefaultValues(); + expectedRuleModel.setConditions(rulesUtils.createVariousConditions()); + expectedRuleModel.setTriggers(List.of("inbound")); + restClient.assertStatusCodeIs(CREATED); + rule.assertThat().isEqualTo(expectedRuleModel, ID, IS_SHARED); + } + + /** + * Check we can create a rule with empty list as conditions + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithConditions_emptyConditionList() + { + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + ruleModel.setConditions(rulesUtils.createCompositeCondition(null)); + + RestRuleModel rule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + RestRuleModel expectedRuleModel = rulesUtils.createRuleModelWithDefaultValues(); + expectedRuleModel.setTriggers(List.of("inbound")); + restClient.assertStatusCodeIs(CREATED); + rule.assertThat().isEqualTo(expectedRuleModel, ID, IS_SHARED); + } + + /** + * Check we can NOT create a rule when category ID in condition is invalid, HTTP status code 400 is expected + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void createRuleWithConditions_invalidCategory() + { + STEP("Try to create a rule with non existing category in conditions."); + String fakeCategoryId = "bdba5f9f-fake-id22-803b-349bcfd06fd1"; + RestCompositeConditionDefinitionModel conditions = rulesUtils.createCompositeCondition(List.of( + rulesUtils.createCompositeCondition(!INVERTED, List.of( + rulesUtils.createSimpleCondition("category", "equals", fakeCategoryId) + )) + )); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + ruleModel.setConditions(conditions); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary("Category in condition is invalid"); + } + + private String getAddPermissionsBody(String username, String role) + { + JsonObject userPermission = Json.createObjectBuilder().add("permissions", + Json.createObjectBuilder() + .add("isInheritanceEnabled", true) + .add("locallySet", Json.createObjectBuilder() + .add("authorityId", username) + .add("name", role).add("accessStatus", "ALLOWED"))) + .build(); + return userPermission.toString(); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/DeleteRulesTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/DeleteRulesTests.java new file mode 100644 index 0000000000..94b36eabce --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/DeleteRulesTests.java @@ -0,0 +1,253 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rules; + +import static java.util.stream.Collectors.toList; + +import static org.alfresco.utility.constants.UserRole.SiteCollaborator; +import static org.alfresco.utility.constants.UserRole.SiteContributor; +import static org.alfresco.utility.constants.UserRole.SiteManager; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static org.springframework.http.HttpStatus.OK; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.alfresco.rest.RestTest; +import org.alfresco.rest.model.RestRuleModel; +import org.alfresco.rest.model.RestRuleModelsCollection; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.junit.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Tests for DELETE /nodes/{nodeId}/rule-sets/{ruleSetId}/rules/{ruleId}. + */ +@Test(groups = {TestGroup.RULES}) +public class DeleteRulesTests extends RestTest +{ + private static final String FAKE_NODE_REF = "fake-node-id"; + + private UserModel user; + private SiteModel site; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + STEP("Create a Contributor user and a public site"); + user = dataUser.createRandomTestUser(); + user.setUserRole(SiteContributor); + site = dataSite.usingUser(user).createPublicRandomSite(); + } + + /** + * Delete previously created rule by its id (as Contributor). + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY}) + public void deleteSingleRuleAndGet204() + { + STEP("Create a few rules in the folder"); + final FolderModel ruleFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + final List createdRules = Stream.of("ruleA", "ruleB", "ruleC") + .map(ruleName -> { + RestRuleModel ruleModel = rulesUtils.createRuleModel(ruleName); + return restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + }) + .collect(toList()); + + STEP("Attempt delete one rule"); + final RestRuleModel ruleA = createdRules.get(0); + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet().deleteRule(ruleA.getId()); + restClient.assertStatusCodeIs(NO_CONTENT); + + STEP("Get and check the rules from the folder after deleting one of them"); + final RestRuleModelsCollection rulesAfterDeletion = + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet().getListOfRules(); + restClient.assertStatusCodeIs(OK); + rulesAfterDeletion.assertThat().entriesListCountIs(createdRules.size() - 1); + Assert.assertTrue(rulesAfterDeletion.getEntries() + .stream() + .noneMatch(r -> r.onModel().getId().equals(ruleA.getId())) + ); + final Set ruleIdsThatShouldBeLeft = createdRules.stream() + .filter(r -> !r.getName().equals("ruleA")) + .map(RestRuleModel::getId) + .collect(Collectors.toSet()); + final Set ruleIdsAfterDeletion = rulesAfterDeletion.getEntries().stream() + .map(r -> r.onModel().getId()) + .collect(Collectors.toSet()); + Assert.assertEquals(ruleIdsThatShouldBeLeft, ruleIdsAfterDeletion); + } + + /** + * Try to delete a rule in a non-existing folder and get 404. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void deleteRuleInNonExistingFolderAndGet404() + { + final FolderModel ruleFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + final RestRuleModel testRule = createRule(ruleFolder); + + STEP("Create a non-existing folder model"); + final FolderModel nonExistingFolder = new FolderModel(); + nonExistingFolder.setNodeRef(FAKE_NODE_REF); + + STEP("Attempt delete the rule in non-existing folder"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(nonExistingFolder).usingDefaultRuleSet().deleteRule(testRule.getId()); + + restClient.assertLastError().statusCodeIs(NOT_FOUND); + } + + /** + * Try to delete a rule in a non-existing rule set and get 404. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void deleteRuleInNonExistingRuleSetAndGet404() + { + final FolderModel ruleFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + final RestRuleModel testRule = createRule(ruleFolder); + + STEP("Attempt delete the rule in non-existing rule set"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingRuleSet(FAKE_NODE_REF).deleteRule(testRule.getId()); + + restClient.assertLastError().statusCodeIs(NOT_FOUND); + } + + /** + * Try to delete a non-existing rule and get 404. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY}) + public void deleteNonExistingRuleAndGet404() + { + final FolderModel ruleFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + STEP("Attempt delete non-existing rule"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet().deleteRule(FAKE_NODE_REF); + + restClient.assertLastError().statusCodeIs(NOT_FOUND); + } + + /** + * Try to delete an existing rule passing a wrong but existing folder and get 404. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY}) + public void deleteExistingRuleFromWrongFolderAndGet404() + { + final FolderModel ruleFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + final RestRuleModel testRule = createRule(ruleFolder); + + STEP("Create a second folder in the site"); + final FolderModel anotherFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + + STEP("Attempt delete an existing rule from a wrong but existing (second) folder"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(anotherFolder).usingDefaultRuleSet().deleteRule(testRule.getId()); + + restClient.assertLastError().statusCodeIs(NOT_FOUND); + } + + /** + * Check that a user without write permission on folder cannot delete a rule inside it. + */ + public void deleteSinglePrivateRuleWithoutPermissionAndGet403() + { + STEP("Create a user and use them to create a private site containing a folder with a rule"); + final UserModel privateUser = dataUser.createRandomTestUser(); + final SiteModel privateSite = dataSite.usingUser(privateUser).createPrivateRandomSite(); + final FolderModel privateFolder = dataContent.usingUser(privateUser).usingSite(privateSite).createFolder(); + final RestRuleModel ruleModel = rulesUtils.createRuleModel("Private site rule"); + final RestRuleModel createdRule = + restClient.authenticateUser(privateUser).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + STEP("Try to delete the rule with another user"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet().deleteRule(createdRule.getId()); + + restClient.assertLastError().statusCodeIs(FORBIDDEN); + } + + /** + * Check that a user with SiteCollaborator permissions on folder can delete a rule inside it. + */ + public void deleteSinglePublicRuleAsCollaboratorAndGet403() + { + STEP("Create a user and use them to create a private site containing a folder with a rule"); + final FolderModel ruleFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + final RestRuleModel testRule = createRule(ruleFolder); + + STEP("Create a manager in the private site"); + final UserModel siteCollaborator = dataUser.createRandomTestUser(); + siteCollaborator.setUserRole(SiteCollaborator); + restClient.authenticateUser(user).withCoreAPI().usingSite(site).addPerson(siteCollaborator); + + STEP("Check the manager can delete the rule"); + restClient.authenticateUser(siteCollaborator).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .deleteRule(testRule.getId()); + + restClient.assertLastError().statusCodeIs(FORBIDDEN); + } + + /** + * Check that a user with SiteManager permissions on folder can delete a rule inside it. + */ + public void deleteSinglePrivateRuleAsSiteManagerAndGet204() + { + STEP("Create a user and use them to create a private site containing a folder with a rule"); + final UserModel privateUser = dataUser.createRandomTestUser(); + final SiteModel privateSite = dataSite.usingUser(privateUser).createPrivateRandomSite(); + final FolderModel privateFolder = dataContent.usingUser(privateUser).usingSite(privateSite).createFolder(); + final RestRuleModel ruleModel = rulesUtils.createRuleModel("Private site rule"); + final RestRuleModel createdRule = + restClient.authenticateUser(privateUser).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + STEP("Create a manager in the private site"); + final UserModel siteManager = dataUser.createRandomTestUser(); + siteManager.setUserRole(SiteManager); + restClient.authenticateUser(privateUser).withCoreAPI().usingSite(privateSite).addPerson(siteManager); + + STEP("Check the manager can delete the rule"); + restClient.authenticateUser(siteManager).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet() + .deleteRule(createdRule.getId()); + + restClient.assertStatusCodeIs(NO_CONTENT); + } + + private RestRuleModel createRule(FolderModel ruleFolder) + { + STEP("Create a rule in the folder"); + final RestRuleModel ruleModel = rulesUtils.createRuleModel("Test rule"); + return restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/ExecuteRulesTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/ExecuteRulesTests.java new file mode 100644 index 0000000000..39764a0379 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/ExecuteRulesTests.java @@ -0,0 +1,324 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rules; + +import static org.alfresco.rest.rules.RulesTestsUtils.AUDIO_ASPECT; +import static org.alfresco.rest.rules.RulesTestsUtils.LOCKABLE_ASPECT; +import static org.alfresco.rest.rules.RulesTestsUtils.RULE_NAME_DEFAULT; +import static org.alfresco.utility.report.log.Step.STEP; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.alfresco.dataprep.CMISUtil; +import org.alfresco.rest.RestTest; +import org.alfresco.rest.model.RestActionBodyExecTemplateModel; +import org.alfresco.rest.model.RestNodeModel; +import org.alfresco.rest.model.RestRuleExecutionModel; +import org.alfresco.rest.model.RestRuleModel; +import org.alfresco.utility.constants.UserRole; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.springframework.http.HttpStatus; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * Tests for POST /nodes/{nodeId}/rule-executions. + */ +@Test(groups = { TestGroup.RULES}) +public class ExecuteRulesTests extends RestTest +{ + + private UserModel user; + private SiteModel site; + private FolderModel parentFolder; + private FolderModel childFolder; + private FileModel parentFolderFile; + private FileModel childFolderFile; + private RestRuleModel parentFolderRule; + private RestRuleModel childFolderRule; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + STEP("Create user and a site"); + user = dataUser.createRandomTestUser(); + site = dataSite.usingUser(user).createPublicRandomSite(); + } + + @BeforeMethod(alwaysRun = true) + public void setUp() + { + STEP("Create parent folder, rule folder and file in it"); + parentFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + childFolder = dataContent.usingUser(user).usingResource(parentFolder).createFolder(); + parentFolderFile = dataContent.usingUser(user).usingResource(parentFolder).createContent(CMISUtil.DocumentType.TEXT_PLAIN); + childFolderFile = dataContent.usingUser(user).usingResource(childFolder).createContent(CMISUtil.DocumentType.TEXT_PLAIN); + + STEP("Create rules for parent and rule folders"); + RestActionBodyExecTemplateModel addLockableAspectAction = rulesUtils.createAddAspectAction(LOCKABLE_ASPECT); + RestRuleModel ruleModel = rulesUtils.createRuleModel(RULE_NAME_DEFAULT, List.of(addLockableAspectAction)); + ruleModel.setIsInheritable(true); + parentFolderRule = restClient.authenticateUser(user).withPrivateAPI().usingNode(parentFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + childFolderRule = restClient.authenticateUser(user).withPrivateAPI().usingNode(childFolder).usingDefaultRuleSet().createSingleRule(rulesUtils.createRuleModelWithDefaultValues()); + } + + /** + * Execute one rule with one action trying to add audio aspect to a file. + */ + @Test(groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.ACTIONS, TestGroup.SANITY }) + public void executeRules_onlyOwnedRules() + { + STEP("Check if file aspects don't contain Audio one"); + RestNodeModel fileNode = restClient.authenticateUser(user).withCoreAPI().usingNode(childFolderFile).getNode(); + restClient.assertStatusCodeIs(HttpStatus.OK); + rulesUtils.assertThat(fileNode).notContainsAspects(AUDIO_ASPECT); + + STEP("Execute rule"); + RestRuleExecutionModel executionResult = restClient.authenticateUser(user).withPrivateAPI().usingNode(childFolder).executeRules(rulesUtils.createRuleExecutionRequest()); + restClient.assertStatusCodeIs(HttpStatus.CREATED); + executionResult.assertThat().field("isEachSubFolderIncluded").is(false); + + STEP("Check if only Audio aspect was added"); + fileNode = restClient.authenticateUser(user).withCoreAPI().usingNode(childFolderFile).getNode(); + restClient.assertStatusCodeIs(HttpStatus.OK); + rulesUtils.assertThat(fileNode).containsAspects(AUDIO_ASPECT); + } + + /** + * Execute owned rule adding Audio aspect and inherited rule adding Lockable aspect. + */ + @Test(groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.ACTIONS, TestGroup.SANITY }) + public void executeRules_includeInheritedRules() + { + STEP("Check if file aspects don't contain Audio and Lockable ones"); + RestNodeModel fileNode = restClient.authenticateUser(user).withCoreAPI().usingNode(childFolderFile).getNode(); + restClient.assertStatusCodeIs(HttpStatus.OK); + rulesUtils.assertThat(fileNode).notContainsAspects(AUDIO_ASPECT, LOCKABLE_ASPECT); + + STEP("Execute rules including inherited rules"); + RestRuleExecutionModel ruleExecutionRequest = rulesUtils.createRuleExecutionRequest(); + RestRuleExecutionModel executionResult = restClient.authenticateUser(user).withPrivateAPI().usingNode(childFolder).executeRules(ruleExecutionRequest); + restClient.assertStatusCodeIs(HttpStatus.CREATED); + executionResult.assertThat().field("isEachSubFolderIncluded").is(false); + + STEP("Check if Audio and Lockable aspects were added"); + fileNode = restClient.authenticateUser(user).withCoreAPI().usingNode(childFolderFile).getNode(); + restClient.assertStatusCodeIs(HttpStatus.OK); + rulesUtils.assertThat(fileNode).containsAspects(AUDIO_ASPECT, LOCKABLE_ASPECT); + } + + /** + * Execute rules on parent folder (add Lockable aspect) including sub-folder folders (add Audio aspect). + */ + @Test(groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.ACTIONS, TestGroup.SANITY }) + public void executeRules_includeSubFolderRules() + { + STEP("Check if parent folder's file aspects don't contain Audio and Lockable ones"); + RestNodeModel fileNode = restClient.authenticateUser(user).withCoreAPI().usingNode(parentFolderFile).getNode(); + restClient.assertStatusCodeIs(HttpStatus.OK); + rulesUtils.assertThat(fileNode).notContainsAspects(AUDIO_ASPECT, LOCKABLE_ASPECT); + + STEP("Check if child folder's file aspects don't contain Audio and Lockable ones"); + fileNode = restClient.authenticateUser(user).withCoreAPI().usingNode(childFolderFile).getNode(); + restClient.assertStatusCodeIs(HttpStatus.OK); + rulesUtils.assertThat(fileNode).notContainsAspects(AUDIO_ASPECT, LOCKABLE_ASPECT); + + STEP("Execute rules on parent folder including sub-folders"); + RestRuleExecutionModel ruleExecutionRequest = rulesUtils.createRuleExecutionRequest(); + ruleExecutionRequest.setIsEachSubFolderIncluded(true); + RestRuleExecutionModel executionResult = restClient.authenticateUser(user).withPrivateAPI().usingNode(parentFolder).executeRules(ruleExecutionRequest); + restClient.assertStatusCodeIs(HttpStatus.CREATED); + executionResult.assertThat().field("isEachSubFolderIncluded").is(true); + + STEP("Check if Lockable aspects was added to parent folder's file"); + fileNode = restClient.authenticateUser(user).withCoreAPI().usingNode(parentFolderFile).getNode(); + restClient.assertStatusCodeIs(HttpStatus.OK); + rulesUtils.assertThat(fileNode) + .containsAspects(LOCKABLE_ASPECT) + .notContainsAspects(AUDIO_ASPECT); + + STEP("Check if Audio and Lockable aspects were added to child folder's file"); + fileNode = restClient.authenticateUser(user).withCoreAPI().usingNode(childFolderFile).getNode(); + restClient.assertStatusCodeIs(HttpStatus.OK); + rulesUtils.assertThat(fileNode) + .containsAspects(AUDIO_ASPECT, LOCKABLE_ASPECT); + } + + /** + * Try to execute disabled rule and check if nothing changed. + */ + @Test(groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.ACTIONS }) + public void executeRules_disabledRule() + { + STEP("Disable child rules"); + RestRuleModel updatedChildRule = rulesUtils.createRuleModelWithDefaultValues(); + updatedChildRule.setIsEnabled(false); + restClient.authenticateUser(user).withPrivateAPI().usingNode(childFolder).usingDefaultRuleSet().updateRule(childFolderRule.getId(), updatedChildRule); + + STEP("Check if file aspects don't contain Audio one"); + RestNodeModel fileNode = restClient.authenticateUser(user).withCoreAPI().usingNode(childFolderFile).getNode(); + restClient.assertStatusCodeIs(HttpStatus.OK); + rulesUtils.assertThat(fileNode).notContainsAspects(AUDIO_ASPECT); + + STEP("Execute rule"); + RestRuleExecutionModel executionResult = restClient.authenticateUser(user).withPrivateAPI().usingNode(childFolder).executeRules(rulesUtils.createRuleExecutionRequest()); + restClient.assertStatusCodeIs(HttpStatus.CREATED); + executionResult.assertThat().field("isEachSubFolderIncluded").is(false); + + STEP("Check if Audio aspect is still missing"); + fileNode = restClient.authenticateUser(user).withCoreAPI().usingNode(childFolderFile).getNode(); + restClient.assertStatusCodeIs(HttpStatus.OK); + rulesUtils.assertThat(fileNode).notContainsAspects(AUDIO_ASPECT); + } + + /** + * Try to execute inherited parent folder's rule which is not inheritable. + */ + @Test(groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.ACTIONS }) + public void executeRules_notInheritableRule() + { + STEP("Set parent rule as not inheritable"); + RestRuleModel updatedParentRule = rulesUtils.createRuleModelWithDefaultValues(); + updatedParentRule.setIsInheritable(false); + restClient.authenticateUser(user).withPrivateAPI().usingNode(parentFolder).usingDefaultRuleSet().updateRule(parentFolderRule.getId(), updatedParentRule); + + STEP("Check if file aspects don't contain Audio and Lockable ones"); + RestNodeModel fileNode = restClient.authenticateUser(user).withCoreAPI().usingNode(childFolderFile).getNode(); + restClient.assertStatusCodeIs(HttpStatus.OK); + rulesUtils.assertThat(fileNode).notContainsAspects(AUDIO_ASPECT, LOCKABLE_ASPECT); + + STEP("Execute child folder rules including inherited rules"); + RestRuleExecutionModel executionResult = restClient.authenticateUser(user).withPrivateAPI().usingNode(childFolder).executeRules(rulesUtils.createRuleExecutionRequest()); + restClient.assertStatusCodeIs(HttpStatus.CREATED); + executionResult.assertThat().field("isEachSubFolderIncluded").is(false); + + STEP("Check if Audio aspect is present and Lockable is still missing"); + fileNode = restClient.authenticateUser(user).withCoreAPI().usingNode(childFolderFile).getNode(); + restClient.assertStatusCodeIs(HttpStatus.OK); + rulesUtils.assertThat(fileNode) + .containsAspects(AUDIO_ASPECT) + .notContainsAspects(LOCKABLE_ASPECT); + } + + /** + * Try to execute private folder's rules by user not added to site and receive 403. + */ + @Test(groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.ACTIONS }) + public void executeRules_privateFolderResultsWith403() + { + STEP("Using admin create private site, folder and rule"); + SiteModel privateSite = dataSite.usingAdmin().createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingAdmin().usingSite(privateSite).createFolder(); + dataContent.usingAdmin().usingResource(privateFolder).createContent(CMISUtil.DocumentType.TEXT_PLAIN); + restClient.authenticateUser(dataUser.getAdminUser()).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet().createSingleRule(rulesUtils.createRuleModelWithDefaultValues()); + + STEP("Try to execute private folder's rules by user"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(privateFolder).executeRules(rulesUtils.createRuleExecutionRequest()); + restClient.assertStatusCodeIs(HttpStatus.FORBIDDEN); + } + + /** + * Try to execute private folder's rules as site contributor and receive 403. + */ + @Test(groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.ACTIONS }) + public void executeRules_privateFolderAsContributorResultsWith403() + { + STEP("Using admin create private site, folder, file in it, rule and add user to site as contributor"); + UserModel contributor = dataUser.createRandomTestUser(); + SiteModel privateSite = dataSite.usingAdmin().createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingAdmin().usingSite(privateSite).createFolder(); + dataContent.usingAdmin().usingResource(privateFolder).createContent(CMISUtil.DocumentType.TEXT_PLAIN); + restClient.authenticateUser(dataUser.getAdminUser()).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet().createSingleRule(rulesUtils.createRuleModelWithDefaultValues()); + dataUser.usingAdmin().addUserToSite(contributor, privateSite, UserRole.SiteContributor); + + STEP("Try to execute private folder's rules by contributor"); + restClient.authenticateUser(contributor).withPrivateAPI().usingNode(privateFolder).executeRules(rulesUtils.createRuleExecutionRequest()); + restClient.assertStatusCodeIs(HttpStatus.FORBIDDEN); + } + + /** + * Execute private folder's rules as site collaborator. + */ + @Test(groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.ACTIONS }) + public void executeRules_privateFolderAsCollaborator() + { + STEP("Using admin create private site, folder, file in it, rule and add user to site as collaborator"); + UserModel collaborator = dataUser.createRandomTestUser(); + UserModel admin = dataUser.getAdminUser(); + SiteModel privateSite = dataSite.usingAdmin().createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingAdmin().usingSite(privateSite).createFolder(); + FileModel privateFile = dataContent.usingAdmin().usingResource(privateFolder).createContent(CMISUtil.DocumentType.TEXT_PLAIN); + restClient.authenticateUser(admin).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet().createSingleRule(rulesUtils.createRuleModelWithDefaultValues()); + dataUser.usingAdmin().addUserToSite(collaborator, privateSite, UserRole.SiteCollaborator); + + STEP("Check if file aspects don't contain Audio one"); + RestNodeModel fileNode = restClient.authenticateUser(admin).withCoreAPI().usingNode(privateFile).getNode(); + restClient.assertStatusCodeIs(HttpStatus.OK); + rulesUtils.assertThat(fileNode).notContainsAspects(AUDIO_ASPECT); + + STEP("Execute private folder's rules by collaborator"); + restClient.authenticateUser(collaborator).withPrivateAPI().usingNode(privateFolder).executeRules(rulesUtils.createRuleExecutionRequest()); + restClient.assertStatusCodeIs(HttpStatus.CREATED); + + STEP("Check if Audio aspect is present"); + fileNode = restClient.authenticateUser(admin).withCoreAPI().usingNode(privateFile).getNode(); + restClient.assertStatusCodeIs(HttpStatus.OK); + rulesUtils.assertThat(fileNode).containsAspects(AUDIO_ASPECT); + } + + /** + * Try to execute rule with broken action and receive 404 error. + */ + @Test(groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.ACTIONS }) + public void executeRules_copyActionWithDeletedDestinationFolder() + { + FolderModel owningFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + FileModel owningFolderFile = dataContent.usingUser(user).usingResource(owningFolder).createContent(CMISUtil.DocumentType.TEXT_PLAIN); + FolderModel destinationFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + + STEP("Create copy action and rule"); + final Map copyParams = + Map.of("destination-folder", destinationFolder.getNodeRef(), "deep-copy", true); + final RestActionBodyExecTemplateModel copyAction = rulesUtils.createCustomActionModel("copy", copyParams); + final RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + ruleModel.setActions(Arrays.asList(copyAction)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(owningFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + + STEP("Delete destination folder and execute rule"); + restClient.authenticateUser(user).withCoreAPI().usingNode(destinationFolder).deleteNode(destinationFolder.getNodeRef()); + restClient.authenticateUser(user).withPrivateAPI().usingNode(owningFolder).executeRules(rulesUtils.createRuleExecutionRequest()); + restClient.assertStatusCodeIs(HttpStatus.NOT_FOUND); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetInheritedRulesTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetInheritedRulesTests.java new file mode 100644 index 0000000000..4267c5aa76 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetInheritedRulesTests.java @@ -0,0 +1,141 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rules; + +import static org.alfresco.utility.report.log.Step.STEP; +import static org.testng.Assert.assertEquals; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.alfresco.rest.RestTest; +import org.alfresco.rest.model.RestRuleModel; +import org.alfresco.rest.model.RestRuleModelsCollection; +import org.alfresco.rest.model.RestRuleSetLinkModel; +import org.alfresco.rest.model.RestRuleSetModel; +import org.alfresco.rest.model.RestRuleSetModelsCollection; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Tests for GET /nodes/{nodeId}/rule-sets/{ruleSetId}/rules with rule inheritance. + */ +@Test(groups = {TestGroup.RULES}) +public class GetInheritedRulesTests extends RestTest +{ + private UserModel user; + private SiteModel site; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + STEP("Create a user and site"); + user = dataUser.createRandomTestUser(); + site = dataSite.usingUser(user).createPublicRandomSite(); + } + + /** + * Check we can get all the rules for the folder by providing the different rule set ids. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void getInheritedRules() + { + STEP("Create a parent and child folder, each with inheriting rules"); + FolderModel parent = dataContent.usingUser(user).usingSite(site).createFolder(); + FolderModel child = dataContent.usingUser(user).usingResource(parent).createFolder(); + RestRuleModel parentRule = rulesUtils.createRuleModelWithDefaultValues(); + parentRule = restClient.authenticateUser(user).withPrivateAPI().usingNode(parent).usingDefaultRuleSet().createSingleRule(parentRule); + RestRuleModel childRule = rulesUtils.createRuleModelWithDefaultValues(); + childRule = restClient.authenticateUser(user).withPrivateAPI().usingNode(child).usingDefaultRuleSet().createSingleRule(childRule); + + STEP("Get the rules in the default rule set for the child folder"); + RestRuleModelsCollection rules = restClient.authenticateUser(user).withPrivateAPI().usingNode(child).usingDefaultRuleSet().getListOfRules(); + rules.assertThat().entriesListContains("id", childRule.getId()) + .and().entriesListCountIs(1); + + STEP("Get the rules in the inherited rule set for the child folder"); + RestRuleSetModelsCollection ruleSets = restClient.authenticateUser(user).withPrivateAPI().usingNode(child).include("inclusionType").getListOfRuleSets(); + String inheritedRuleSetId = ruleSets.getEntries().stream() + .filter(ruleSet -> ruleSet.onModel().getInclusionType().equals("inherited")) + .findFirst().get().onModel().getId(); + RestRuleModelsCollection inheritedRules = restClient.authenticateUser(user).withPrivateAPI().usingNode(child).usingRuleSet(inheritedRuleSetId).getListOfRules(); + inheritedRules.assertThat().entriesListContains("id", parentRule.getId()) + .and().entriesListCountIs(1); + } + + /** + * Check that we only get each rule once with linking and inheritance, and the order is correct. + *

+ * The folder structure for this test is as follows: + *

+     *      A --[links]-> DRuleSet
+     *      +-B --[owns]-> BRuleSet
+     *        +-C --[owns]-> CRuleSet
+     *          +-D --[owns]--> DRuleSet
+     * 
+ */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void rulesReturnedAreUnique() + { + STEP("Create four folders with rules"); + FolderModel folderA = dataContent.usingUser(user).usingSite(site).createFolder(); + FolderModel folderB = dataContent.usingUser(user).usingResource(folderA).createFolder(); + FolderModel folderC = dataContent.usingUser(user).usingResource(folderB).createFolder(); + FolderModel folderD = dataContent.usingUser(user).usingResource(folderC).createFolder(); + RestRuleModel ruleB = restClient.authenticateUser(user).withPrivateAPI().usingNode(folderB).usingDefaultRuleSet().createSingleRule(rulesUtils.createRuleModelWithDefaultValues()); + RestRuleModel ruleC = restClient.authenticateUser(user).withPrivateAPI().usingNode(folderC).usingDefaultRuleSet().createSingleRule(rulesUtils.createRuleModelWithDefaultValues()); + RestRuleModel ruleD = restClient.authenticateUser(user).withPrivateAPI().usingNode(folderD).usingDefaultRuleSet().createSingleRule(rulesUtils.createRuleModelWithDefaultValues()); + STEP("Link folderA to ruleSetD"); + RestRuleSetLinkModel linkModel = new RestRuleSetLinkModel(); + linkModel.setId(folderD.getNodeRef()); + restClient.authenticateUser(user).withPrivateAPI().usingNode(folderA).createRuleLink(linkModel); + + STEP("Get the rule sets for the folderD"); + List ruleSets = restClient.authenticateUser(user).withPrivateAPI().usingNode(folderD).getListOfRuleSets().getEntries(); + + STEP("Check the rules for each rule set are as expected"); + List expectedRuleIds = List.of(ruleD, ruleB, ruleC); + IntStream.range(0, 2).forEach(index -> { + String ruleSetId = ruleSets.get(index).onModel().getId(); + List rules = restClient.authenticateUser(user) + .withPrivateAPI() + .usingNode(folderD) + .usingRuleSet(ruleSetId) + .getListOfRules() + .getEntries() + .stream() + .map(RestRuleModel::onModel) + .collect(Collectors.toList()); + assertEquals(rules, List.of(expectedRuleIds.get(index)), "Unexpected rules found for rule set " + ruleSetId); + }); + assertEquals(ruleSets.size(), 3, "Expected three unique rule sets to be returned but got " + ruleSets); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetRuleSetsTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetRuleSetsTests.java new file mode 100644 index 0000000000..aed3d9f0b0 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetRuleSetsTests.java @@ -0,0 +1,601 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rules; + +import static org.alfresco.rest.requests.RuleSettings.IS_INHERITANCE_ENABLED; +import static org.alfresco.rest.rules.RulesTestsUtils.MOVE_ACTION; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.junit.Assert.assertTrue; +import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.OK; + +import java.util.List; + +import com.google.common.collect.ImmutableMap; + +import org.alfresco.rest.RestTest; +import org.alfresco.rest.model.RestRuleModel; +import org.alfresco.rest.model.RestRuleSetLinkModel; +import org.alfresco.rest.model.RestRuleSetModel; +import org.alfresco.rest.model.RestRuleSetModelsCollection; +import org.alfresco.rest.model.RestRuleSettingsModel; +import org.alfresco.rest.requests.coreAPI.RestCoreAPI; +import org.alfresco.rest.requests.privateAPI.RestPrivateAPI; +import org.alfresco.utility.constants.UserRole; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Tests for GET /nodes/{nodeId}/rule-sets and /nodes/{nodeId}/rule-sets/{ruleSetId}. + */ +@Test (groups = { TestGroup.RULES }) +public class GetRuleSetsTests extends RestTest +{ + private UserModel user; + private SiteModel site; + private FolderModel ruleFolder; + /** A folder with a rule in a private site owned by admin. */ + private FolderModel privateFolder; + private FolderModel inheritingChildFolder; + private FolderModel notInheritingChildFolder; + private RestRuleModel rule; + private String ruleSetId; + + @BeforeClass (alwaysRun = true) + public void dataPreparation() + { + STEP("Create a user, site and folder."); + user = dataUser.createRandomTestUser(); + site = dataSite.usingUser(user).createPublicRandomSite(); + ruleFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + + STEP("Create two children of the folder - one that inherits rules and one that doesn't"); + inheritingChildFolder = dataContent.usingUser(user).usingResource(ruleFolder).createFolder(); + notInheritingChildFolder = dataContent.usingUser(user).usingResource(ruleFolder).createFolder(); + RestRuleSettingsModel doesntInherit = new RestRuleSettingsModel(); + doesntInherit.setValue(false); + restClient.authenticateUser(user).withPrivateAPI().usingNode(notInheritingChildFolder) + .usingIsInheritanceEnabledRuleSetting().updateSetting(doesntInherit); + + STEP("Create a rule in the folder."); + RestRuleModel ruleModel = rulesUtils.createRuleModel("ruleName"); + rule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + STEP("Get the rule sets for the folder and find the rule set id"); + RestRuleSetModelsCollection ruleSets = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder) + .getListOfRuleSets(); + ruleSets.assertThat().entriesListCountIs(1); + ruleSetId = ruleSets.getEntries().get(0).onModel().getId(); + + STEP("Use admin to create a private site containing a rule in a rule set that can be inherited."); + SiteModel privateSite = dataSite.usingAdmin().createPrivateRandomSite(); + privateFolder = dataContent.usingAdmin().usingSite(privateSite).createFolder(); + privateAPIForAdmin().usingNode(privateFolder).usingDefaultRuleSet().createSingleRule(rulesUtils.createRuleModelWithModifiedValues()); + } + + /** Check we can get an empty list of rule sets. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getEmptyRuleSetsList() + { + STEP("Create a folder in existing site"); + FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + + STEP("Get the rule sets for the folder"); + RestRuleSetModelsCollection ruleSets = restClient.authenticateUser(user).withPrivateAPI() + .usingNode(folder).getListOfRuleSets(); + + restClient.assertStatusCodeIs(OK); + assertTrue("Expected no rule sets to be present.", ruleSets.isEmpty()); + } + + /** Check we can get a list of rule sets. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void getRuleSetsList() + { + STEP("Get the rule sets for the folder"); + RestRuleSetModelsCollection ruleSets = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder) + .getListOfRuleSets(); + + restClient.assertStatusCodeIs(OK); + ruleSets.assertThat().entriesListCountIs(1); + ruleSets.getEntries().get(0).onModel() + .assertThat().field("id").isNotNull(); + } + + /** Check we get a 404 if trying to load rule sets for a folder that doesn't exist. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getRuleSetsForNonExistentFolder() + { + STEP("Try to load rule sets for a non-existent folder."); + FolderModel nonExistentFolder = FolderModel.getRandomFolderModel(); + nonExistentFolder.setNodeRef("fake-id"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(nonExistentFolder).getListOfRuleSets(); + restClient.assertStatusCodeIs(NOT_FOUND); + } + + /** Check that we get a 403 error when trying to get rule sets for a folder we don't have read access to. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getRuleSetsWithoutPermission() + { + STEP("Check a user cannot list rule sets without read access."); + privateAPIForUser().usingNode(privateFolder).getListOfRuleSets(); + restClient.assertStatusCodeIs(FORBIDDEN); + } + + /** Check that we can still list some rule sets if we don't have permission to view them all. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void permissionsAreRespectedWhenListingRuleSets() + { + STEP("Create a public site containing a parent and child folder with rule inheritance enabled."); + SiteModel publicSite = dataSite.usingUser(user).createPublicRandomSite(); + FolderModel parentFolder = dataContent.usingUser(user).usingSite(publicSite).createFolder(); + FolderModel childFolder = dataContent.usingUser(user).usingResource(parentFolder).createFolder(); + RestRuleSettingsModel enabled = new RestRuleSettingsModel(); + enabled.setValue(true); + privateAPIForUser().usingNode(parentFolder).usingRuleSetting(IS_INHERITANCE_ENABLED).updateSetting(enabled); + + STEP("Link the parent folder to a private rule set."); + RestRuleSetLinkModel linkModel = new RestRuleSetLinkModel(); + linkModel.setId(privateFolder.getNodeRef()); + privateAPIForAdmin().usingNode(parentFolder).createRuleLink(linkModel); + + STEP("Create a rule on the child folder."); + privateAPIForUser().usingNode(childFolder).usingDefaultRuleSet().createSingleRule(rulesUtils.createRuleModelWithDefaultValues()); + + STEP("Check admin can view both rule sets."); + RestRuleSetModelsCollection adminViewOfRuleSets = privateAPIForAdmin().usingNode(childFolder).getListOfRuleSets(); + restClient.assertStatusCodeIs(OK); + RestRuleSetModel parentRuleSet = adminViewOfRuleSets.getEntries().get(0).onModel(); + RestRuleSetModel childRuleSet = adminViewOfRuleSets.getEntries().get(1).onModel(); + + STEP("Check the normal user can only view the child rule set."); + RestRuleSetModelsCollection userViewOfRuleSets = privateAPIForUser().usingNode(childFolder).getListOfRuleSets(); + restClient.assertStatusCodeIs(OK); + userViewOfRuleSets.assertThat().entriesListContains("id", childRuleSet.getId()) + .and().entriesListDoesNotContain("id", parentRuleSet.getId()); + } + + /** Check we can get the id of the folder that owns a list of rule sets. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getRuleSetsAndOwningFolders() + { + STEP("Get the rule sets and owning folders"); + RestRuleSetModelsCollection ruleSets = restClient.authenticateUser(user).withPrivateAPI() + .usingNode(ruleFolder) + .include("owningFolder") + .getListOfRuleSets(); + + restClient.assertStatusCodeIs(OK); + ruleSets.getEntries().get(0).onModel() + .assertThat().field("owningFolder").is(ruleFolder.getNodeRef()) + .assertThat().field("id").is(ruleSetId); + ruleSets.assertThat().entriesListCountIs(1); + } + + /** Check we can get the reason that a rule set is included in the list. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getRuleSetsAndOwnedInclusionType() + { + STEP("Get the rule sets and inclusion type"); + RestRuleSetModelsCollection ruleSets = restClient.authenticateUser(user).withPrivateAPI() + .usingNode(ruleFolder) + .include("inclusionType") + .getListOfRuleSets(); + + restClient.assertStatusCodeIs(OK); + ruleSets.getEntries().get(0).onModel() + .assertThat().field("inclusionType").is("owned") + .assertThat().field("id").is(ruleSetId); + ruleSets.assertThat().entriesListCountIs(1); + } + + /** Check we can tell that a rule set has been inherited. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getRuleSetsAndInheritedInclusionType() + { + STEP("Get the rule sets and inclusion type"); + RestRuleSetModelsCollection ruleSets = restClient.authenticateUser(user).withPrivateAPI() + .usingNode(inheritingChildFolder) + .include("inclusionType") + .getListOfRuleSets(); + + restClient.assertStatusCodeIs(OK); + ruleSets.getEntries().get(0).onModel() + .assertThat().field("inclusionType").is("inherited") + .assertThat().field("id").is(ruleSetId); + ruleSets.assertThat().entriesListCountIs(1); + } + + /** Check that a rule set is not inherited if inheriting is disabled. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getRuleSetsWithoutInheriting() + { + STEP("Get the rule sets and inclusion type"); + RestRuleSetModelsCollection ruleSets = restClient.authenticateUser(user).withPrivateAPI() + .usingNode(notInheritingChildFolder) + .getListOfRuleSets(); + + restClient.assertStatusCodeIs(OK); + ruleSets.assertThat().entriesListCountIs(0); + } + + /** Check we can get a rule set by its id. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void getRuleSetById() + { + STEP("Get the rule set using its rule set id"); + RestRuleSetModel ruleSet = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder) + .getRuleSet(ruleSetId); + + restClient.assertStatusCodeIs(OK); + ruleSet.assertThat().field("id").is(ruleSetId) + // Also check that the optional fields are not included by default. + .assertThat().field("owningFolder").isNull() + .assertThat().field("inheritedBy").isNull() + .assertThat().field("linkedToBy").isNull() + .assertThat().field("isInherited").isNull() + .assertThat().field("isLinkedTo").isNull(); + } + + /** Check we can get a rule set using the "-default-" synonym. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getDefaultRuleSetById() + { + STEP("Get the default rule set for the folder"); + RestRuleSetModel ruleSet = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder) + .getDefaultRuleSet(); + + restClient.assertStatusCodeIs(OK); + ruleSet.assertThat().field("id").isNotNull(); + } + + /** Check we get a 404 if trying to load the default rule set for a folder that doesn't exist. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getDefaultRuleSetForNonExistentFolder() + { + STEP("Try to load a rule set for a non-existent folder."); + FolderModel nonExistentFolder = FolderModel.getRandomFolderModel(); + nonExistentFolder.setNodeRef("fake-id"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(nonExistentFolder).getDefaultRuleSet(); + restClient.assertStatusCodeIs(NOT_FOUND); + } + + /** Check we get 404 for a non-existing rule set id. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getRuleSetByNonExistingId() + { + STEP("Get the rule set using fake rule set id"); + String fakeRuleSetId = "fake-rule-set-id"; + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).getRuleSet(fakeRuleSetId); + restClient.assertStatusCodeIs(NOT_FOUND); + } + + /** Check we can get the id of the folder that owns a rule set. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getRuleSetAndOwningFolder() + { + STEP("Get the rule set and owning folder"); + RestRuleSetModel ruleSet = restClient.authenticateUser(user).withPrivateAPI() + .usingNode(ruleFolder) + .include("owningFolder") + .getRuleSet(ruleSetId); + + restClient.assertStatusCodeIs(OK); + ruleSet.assertThat().field("owningFolder").is(ruleFolder.getNodeRef()) + .assertThat().field("id").is(ruleSetId); + } + + /** + * Check we can find out the id of any folders that inherit a rule set. + *

+ * The test checks several different situations: + *

+     *   folder --[owns]-> rule set
+     *   +- publicFolder --[inherits]-> rule set (user has access)
+     *   +- privateFolder --[inherits]-> rule set (user does not have access)
+     *      +- publicGrandchild --[inherits]-> rule set (user has access again)
+     *   +- nonInheritingFolder (inheritance should be prevented)
+     *      +- linkingFolder --[links]-> rule set (not inherited)
+     *         +- descendantFolder --[inherits]-> rule set (inherited via link)
+     * 
+ */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getRuleSetAndInheritedBy() + { + STEP("Create a site owned by admin and add user as a contributor"); + SiteModel siteModel = dataSite.usingAdmin().createPrivateRandomSite(); + dataUser.addUserToSite(user, siteModel, UserRole.SiteContributor); + + STEP("Create the folder structure"); + FolderModel folder = dataContent.usingUser(user).usingSite(siteModel).createFolder(); + FolderModel publicFolder = dataContent.usingUser(user).usingResource(folder).createFolder(); + FolderModel privateFolder = dataContent.usingAdmin().usingResource(folder).createFolder(); + dataContent.usingAdmin().usingResource(privateFolder).setInheritPermissions(false); + // Create the grandchild with user and use admin to move it under the private folder. + FolderModel publicGrandchild = dataContent.usingUser(user).usingSite(siteModel).createFolder(); + coreAPIForAdmin().usingActions().executeAction(MOVE_ACTION, publicGrandchild, ImmutableMap.of("destination-folder", "workspace://SpacesStore/" + privateFolder.getNodeRef())); + // Create the non-inheriting folder. + FolderModel nonInheritingFolder = dataContent.usingUser(user).usingResource(folder).createFolder(); + RestRuleSettingsModel nonInheriting = new RestRuleSettingsModel(); + nonInheriting.setKey(IS_INHERITANCE_ENABLED); + nonInheriting.setValue(false); + privateAPIForUser().usingNode(nonInheritingFolder).usingIsInheritanceEnabledRuleSetting().updateSetting(nonInheriting); + // Create a child that will link to the rule and a child of that to inherit via the link. + FolderModel linkingFolder = dataContent.usingUser(user).usingResource(nonInheritingFolder).createFolder(); + FolderModel descendantFolder = dataContent.usingUser(user).usingResource(linkingFolder).createFolder(); + + STEP("Create an inheritable rule in the folder and get the rule set id."); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithModifiedValues(); + privateAPIForUser().usingNode(folder).usingDefaultRuleSet().createSingleRule(ruleModel); + RestRuleSetModelsCollection ruleSets = privateAPIForUser().usingNode(folder).getListOfRuleSets(); + String ruleSetId = ruleSets.getEntries().get(0).onModel().getId(); + + STEP("Create the link to the rule from the linking folder"); + RestRuleSetLinkModel ruleSetLink = new RestRuleSetLinkModel(); + ruleSetLink.setId(folder.getNodeRef()); + privateAPIForUser().usingNode(linkingFolder).createRuleLink(ruleSetLink); + + STEP("Remove the user from the site"); + dataUser.removeUserFromSite(user, siteModel); + + STEP("Get the rule set and inheriting folders"); + RestRuleSetModel ruleSet = privateAPIForUser().usingNode(folder) + .include("inheritedBy") + .getRuleSet(ruleSetId); + + restClient.assertStatusCodeIs(OK); + List expectedInheritors = List.of(publicFolder.getNodeRef(), descendantFolder.getNodeRef(), publicGrandchild.getNodeRef()); + ruleSet.assertThat().field("inheritedBy").is(expectedInheritors) + .assertThat().field("id").is(ruleSetId); + } + + /** Check we can get the folders that link to a rule set and that this respects permissions. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getRuleSetsAndLinkedToBy() + { + STEP("Create a site owned by admin and add user as a contributor"); + SiteModel siteModel = dataSite.usingAdmin().createPrivateRandomSite(); + dataUser.addUserToSite(user, siteModel, UserRole.SiteContributor); + + STEP("Create the folder structure"); + FolderModel ruleFolder = dataContent.usingUser(user).usingSite(siteModel).createFolder(); + FolderModel publicFolder = dataContent.usingUser(user).usingSite(siteModel).createFolder(); + FolderModel privateFolder = dataContent.usingAdmin().usingSite(siteModel).createFolder(); + dataContent.usingAdmin().usingResource(privateFolder).setInheritPermissions(false); + + STEP("Remove the user from the site"); + dataUser.removeUserFromSite(user, siteModel); + + STEP("Create a rule in the folder and link to it from the other two."); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + privateAPIForUser().usingNode(ruleFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + RestRuleSetModelsCollection ruleSets = privateAPIForAdmin().usingNode(ruleFolder).getListOfRuleSets(); + String ruleSetId = ruleSets.getEntries().get(0).onModel().getId(); + RestRuleSetLinkModel ruleSetLink = new RestRuleSetLinkModel(); + ruleSetLink.setId(ruleFolder.getNodeRef()); + privateAPIForUser().usingNode(publicFolder).createRuleLink(ruleSetLink); + privateAPIForAdmin().usingNode(privateFolder).createRuleLink(ruleSetLink); + + STEP("Get the rule set and linkedToBy field"); + RestRuleSetModel ruleSet = privateAPIForUser().usingNode(ruleFolder) + .include("linkedToBy") + .getRuleSet(ruleSetId); + + restClient.assertStatusCodeIs(OK); + ruleSet.assertThat().field("linkedToBy").is(List.of(publicFolder.getNodeRef())) + .assertThat().field("id").is(ruleSetId); + } + + /** Check that a user can see that a rule set is inherited even if they don't have permission to view the inheriting folder. */ + @Test + public void getRuleSetAndIsInheritedWithoutPermission() + { + STEP("Create a site owned by admin and add user as a contributor"); + SiteModel siteModel = dataSite.usingAdmin().createPrivateRandomSite(); + dataUser.addUserToSite(user, siteModel, UserRole.SiteContributor); + + STEP("Create a folder with a rule set and a private child folder to inherit it"); + FolderModel ruleFolder = dataContent.usingUser(user).usingSite(siteModel).createFolder(); + dataContent.usingAdmin().usingResource(ruleFolder).createFolder(); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + privateAPIForUser().usingNode(ruleFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + + STEP("Remove the user from the site"); + dataUser.removeUserFromSite(user, siteModel); + + STEP("Get the rule set and isInherited field"); + RestRuleSetModel ruleSet = privateAPIForUser().usingNode(ruleFolder) + .include("isInherited", "inheritedBy") + .getDefaultRuleSet(); + + restClient.assertStatusCodeIs(OK); + ruleSet.assertThat().field("isInherited").is(true) + .assertThat().field("inheritedBy").isEmpty(); + + } + + /** Check that the isInherited field includes rule sets which are only inherited via links. */ + @Test + public void getRuleSetAndIsInheritedViaLink() + { + STEP("Create a site and a folder with a rule"); + SiteModel siteModel = dataSite.usingUser(user).createPublicRandomSite(); + FolderModel ruleFolder = dataContent.usingUser(user).usingSite(siteModel).createFolder(); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + privateAPIForUser().usingNode(ruleFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + + STEP("Create a second folder in the site that links to the rule set"); + FolderModel secondFolder = dataContent.usingUser(user).usingSite(siteModel).createFolder(); + dataContent.usingUser(user).usingResource(secondFolder).createFolder(); + RestRuleSetLinkModel ruleSetLink = new RestRuleSetLinkModel(); + ruleSetLink.setId(ruleFolder.getNodeRef()); + privateAPIForUser().usingNode(secondFolder).createRuleLink(ruleSetLink); + + STEP("Get the rule set and isInherited field"); + RestRuleSetModel ruleSet = privateAPIForUser().usingNode(ruleFolder) + .include("isInherited") + .getDefaultRuleSet(); + + restClient.assertStatusCodeIs(OK); + ruleSet.assertThat().field("isInherited").is(true); + } + + /** + * Check that if a rule set is owned and linked to but not inherited then isInherited returns false. + */ + @Test + public void getRuleSetAndIsInheritedCanBeFalse() + { + STEP("Create a site and a folder with a rule"); + SiteModel siteModel = dataSite.usingUser(user).createPublicRandomSite(); + FolderModel ruleFolder = dataContent.usingUser(user).usingSite(siteModel).createFolder(); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + privateAPIForUser().usingNode(ruleFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + + STEP("Create a second folder in the site that links to the rule set"); + FolderModel secondFolder = dataContent.usingUser(user).usingSite(siteModel).createFolder(); + RestRuleSetLinkModel ruleSetLink = new RestRuleSetLinkModel(); + ruleSetLink.setId(ruleFolder.getNodeRef()); + privateAPIForUser().usingNode(secondFolder).createRuleLink(ruleSetLink); + + STEP("Get the rule set and isInherited field"); + RestRuleSetModel ruleSet = privateAPIForUser().usingNode(ruleFolder) + .include("isInherited") + .getDefaultRuleSet(); + + restClient.assertStatusCodeIs(OK); + ruleSet.assertThat().field("isInherited").is(false); + } + + + /** Check that a user can see that a rule set is linked to even if they don't have permission to view the linking folder. */ + @Test + public void getRuleSetAndIsLinkedToWithoutPermission() + { + STEP("Create a site owned by admin and add user as a contributor"); + SiteModel siteModel = dataSite.usingAdmin().createPrivateRandomSite(); + dataUser.addUserToSite(user, siteModel, UserRole.SiteContributor); + + STEP("Create a folder with a rule set"); + FolderModel ruleFolder = dataContent.usingUser(user).usingSite(siteModel).createFolder(); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + privateAPIForUser().usingNode(ruleFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + + STEP("Create a private folder linking to the rule set"); + FolderModel linkingFolder = dataContent.usingAdmin().usingSite(siteModel).createFolder(); + RestRuleSetLinkModel linkModel = new RestRuleSetLinkModel(); + linkModel.setId(ruleFolder.getNodeRef()); + privateAPIForAdmin().usingNode(linkingFolder).createRuleLink(linkModel); + + STEP("Remove the user from the site"); + dataUser.removeUserFromSite(user, siteModel); + + STEP("Get the rule set and isLinkedTo field"); + RestRuleSetModel ruleSet = privateAPIForUser().usingNode(ruleFolder) + .include("isLinkedTo", "linkedToBy", "owningFolder") + .getDefaultRuleSet(); + + restClient.assertStatusCodeIs(OK); + ruleSet.assertThat().field("isLinkedTo").is(true) + .assertThat().field("linkedToBy").isEmpty(); + + } + + /** + * Check that if a rule set is owned and inherited but not linked to then isLinkedTo returns false. + */ + @Test + public void getRuleSetAndIsLinkedToCanBeFalse() + { + STEP("Create a site, a folder with a rule and a child folder that inherits it"); + SiteModel siteModel = dataSite.usingUser(user).createPublicRandomSite(); + FolderModel ruleFolder = dataContent.usingUser(user).usingSite(siteModel).createFolder(); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + privateAPIForUser().usingNode(ruleFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + dataContent.usingUser(user).usingResource(ruleFolder).createFolder(); + + STEP("Get the rule set and isLinkedTo field"); + RestRuleSetModel ruleSet = privateAPIForUser().usingNode(ruleFolder) + .include("isLinkedTo") + .getDefaultRuleSet(); + + restClient.assertStatusCodeIs(OK); + ruleSet.assertThat().field("isLinkedTo").is(false); + } + + /** Check that we can only view a rule set if have read permission. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void permissionsChecksForFolderWithPrivateAndPublicRuleSets() + { + STEP("Create a public site containing a parent and child folder with rule inheritance enabled."); + SiteModel publicSite = dataSite.usingUser(user).createPublicRandomSite(); + FolderModel parentFolder = dataContent.usingUser(user).usingSite(publicSite).createFolder(); + FolderModel childFolder = dataContent.usingUser(user).usingResource(parentFolder).createFolder(); + RestRuleSettingsModel enabled = new RestRuleSettingsModel(); + enabled.setValue(true); + privateAPIForUser().usingNode(parentFolder).usingRuleSetting(IS_INHERITANCE_ENABLED).updateSetting(enabled); + + STEP("Link the parent folder to a private rule set."); + RestRuleSetLinkModel linkModel = new RestRuleSetLinkModel(); + linkModel.setId(privateFolder.getNodeRef()); + privateAPIForAdmin().usingNode(parentFolder).createRuleLink(linkModel); + + STEP("Create a rule on the child folder."); + privateAPIForUser().usingNode(childFolder).usingDefaultRuleSet().createSingleRule(rulesUtils.createRuleModelWithDefaultValues()); + + STEP("Use the admin user to get both rule sets."); + RestRuleSetModelsCollection adminViewOfRuleSets = privateAPIForAdmin().usingNode(childFolder).getListOfRuleSets(); + RestRuleSetModel parentRuleSet = adminViewOfRuleSets.getEntries().get(0).onModel(); + RestRuleSetModel childRuleSet = adminViewOfRuleSets.getEntries().get(1).onModel(); + + STEP("Check the normal user can only view the child rule set."); + privateAPIForUser().usingNode(childFolder).getRuleSet(parentRuleSet.getId()); + restClient.assertStatusCodeIs(FORBIDDEN); + privateAPIForUser().usingNode(childFolder).getRuleSet(childRuleSet.getId()); + restClient.assertStatusCodeIs(OK); + } + + private RestCoreAPI coreAPIForAdmin() + { + return restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI(); + } + + private RestPrivateAPI privateAPIForUser() + { + return restClient.authenticateUser(user).withPrivateAPI(); + } + + private RestPrivateAPI privateAPIForAdmin() + { + return restClient.authenticateUser(dataUser.getAdminUser()).withPrivateAPI(); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetRulesTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetRulesTests.java new file mode 100644 index 0000000000..f33a3e8703 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/GetRulesTests.java @@ -0,0 +1,363 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rules; + +import static java.util.stream.Collectors.toList; + +import static org.alfresco.utility.constants.UserRole.SiteCollaborator; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.junit.Assert.assertTrue; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.OK; + +import java.util.List; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.alfresco.rest.RestTest; +import org.alfresco.rest.model.RestRuleModel; +import org.alfresco.rest.model.RestRuleModelsCollection; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Tests for GET /nodes/{nodeId}/rule-sets/{ruleSetId}/rules and GET /nodes/{nodeId}/rule-sets/{ruleSetId}/rules/{ruleId}. + */ +@Test(groups = {TestGroup.RULES}) +public class GetRulesTests extends RestTest +{ + private UserModel user; + private SiteModel site; + private FolderModel ruleFolder; + private List createdRules; + private RestRuleModel createdRuleA; + private static final String IGNORE_ID = "id"; + private static final String IGNORE_IS_SHARED = "isShared"; + private static final String ACTIONS = "actions"; + private static final String CONDITIONS = "conditions"; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + STEP("Create a user, site and folder"); + user = dataUser.createRandomTestUser(); + site = dataSite.usingUser(user).createPublicRandomSite(); + ruleFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + + STEP("Create rules in the folder"); + createdRules = Stream.of("ruleA", "ruleB").map(ruleName -> { + RestRuleModel ruleModel = rulesUtils.createRuleModel(ruleName); + return restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + }).collect(toList()); + createdRuleA = createdRules.get(0); + } + + /** Check we can get an empty list of rules. */ + @Test(groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getEmptyRulesList() + { + STEP("Create a folder in existing site"); + FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + + STEP("Get the rules that apply to the folder"); + RestRuleModelsCollection rules = restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).usingDefaultRuleSet().getListOfRules(); + + restClient.assertStatusCodeIs(NOT_FOUND); + assertTrue("Expected no rules to be present.", rules.isEmpty()); + } + + /** + * Check we can get all the rules for a folder. + *

+ * Also check that the isShared field is not returned when not requested. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void getRulesList() + { + STEP("Get the rules that apply to the folder"); + RestRuleModelsCollection rules = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet().getListOfRules(); + + restClient.assertStatusCodeIs(OK); + rules.assertThat().entriesListCountIs(createdRules.size()); + IntStream.range(0, createdRules.size()).forEach(i -> + rules.getEntries().get(i).onModel() + .assertThat().field("id").is(createdRules.get(i).getId()) + .assertThat().field("name").is(createdRules.get(i).getName()) + .assertThat().field("isShared").isNull()); + } + + /** Check we get a 404 if trying to load rules for a folder that doesn't exist. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getRulesForNonExistentFolder() + { + STEP("Try to load rules for a non-existent folder."); + FolderModel nonExistentFolder = FolderModel.getRandomFolderModel(); + nonExistentFolder.setNodeRef("fake-id"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(nonExistentFolder).usingDefaultRuleSet().getListOfRules(); + restClient.assertStatusCodeIs(NOT_FOUND); + } + + /** Check we get a 404 if trying to load rules with a rule set id that doesn't exist. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getRulesFromNonExistentRuleSet() + { + STEP("Create a folder in existing site"); + FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + STEP("Try to load rules for a non-existent rule set."); + restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).usingRuleSet("fake-id").getListOfRules(); + restClient.assertStatusCodeIs(NOT_FOUND); + } + + /** Check we can get all the rules for a folder along with the extra "include" and "other" fields. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void getRulesListWithIncludedFields() + { + STEP("Get the rules that apply to the folder"); + RestRuleModelsCollection rules = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .include("isShared") + .getListOfRules(); + + rules.assertThat().entriesListCountIs(createdRules.size()); + IntStream.range(0, createdRules.size()).forEach(i -> + rules.getEntries().get(i).onModel() + .assertThat().field("isShared").isNotNull() + .assertThat().field("description").isNull() + .assertThat().field("isEnabled").is(true) + .assertThat().field("isInheritable").is(false) + .assertThat().field("isAsynchronous").is(false) + .assertThat().field("errorScript").isNull() + .assertThat().field("isShared").is(false) + .assertThat().field("triggers").is("[inbound]")); + } + + /** + * Check we can get a rule by its id. + *

+ * Also check that the isShared field is not returned when not requested. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void getSingleRule() + { + STEP("Load a particular rule"); + RestRuleModel rule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet().getSingleRule(createdRuleA.getId()); + + restClient.assertStatusCodeIs(OK); + + rule.assertThat().field("id").is(createdRuleA.getId()) + .assertThat().field("name").is(createdRuleA.getName()) + .assertThat().field("isShared").isNull(); + } + + /** Check we can get rule's other fields */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void getRulesOtherFieldsModified() + { + STEP("Create a rule with all other fields default values modified"); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithModifiedValues(); + ruleModel.setTriggers(List.of("update")); + UserModel admin = dataUser.getAdminUser(); + FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + RestRuleModel rule = restClient.authenticateUser(admin).withPrivateAPI().usingNode(folder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + RestRuleModel expectedRuleModel = rulesUtils.createRuleModelWithModifiedValues(); + expectedRuleModel.setTriggers(List.of("update")); + + restClient.assertStatusCodeIs(CREATED); + rule.assertThat().isEqualTo(expectedRuleModel, IGNORE_ID, IGNORE_IS_SHARED) + .assertThat().field("id").isNotNull() + .assertThat().field("isShared").isNull(); + + } + + /** Check we can get rule's "other" fields */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void getRulesDefaultFields() + { + STEP("Create a rule with all other fields default values"); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + UserModel admin = dataUser.getAdminUser(); + FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + RestRuleModel rule = restClient.authenticateUser(admin).withPrivateAPI().usingNode(folder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + RestRuleModel expectedRuleModel = rulesUtils.createRuleModelWithDefaultValues(); + expectedRuleModel.setTriggers(List.of("inbound")); + + restClient.assertStatusCodeIs(CREATED); + + restClient.assertStatusCodeIs(CREATED); + rule.assertThat().isEqualTo(expectedRuleModel, IGNORE_ID, IGNORE_IS_SHARED) + .assertThat().field("id").isNotNull() + .assertThat().field("isShared").isNull(); + } + + /** Check we get a 404 if trying to load a rule from a folder that doesn't exist. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getSingleRuleFromNonExistentFolder() + { + STEP("Try to load a rule from a non-existent folder."); + FolderModel nonExistentFolder = FolderModel.getRandomFolderModel(); + nonExistentFolder.setNodeRef("fake-id"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(nonExistentFolder).usingDefaultRuleSet().getSingleRule("fake-rule-id"); + restClient.assertStatusCodeIs(NOT_FOUND); + } + + /** Check we get a 404 if trying to load a rule with a rule set id that doesn't exist. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getSingleRuleFromNonExistentRuleSet() + { + STEP("Create a folder in existing site"); + FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + STEP("Try to load rules for a non-existent rule set."); + restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).usingRuleSet("fake-id").getSingleRule("fake-rule-id"); + restClient.assertStatusCodeIs(NOT_FOUND); + } + + /** Check we get a 404 if trying to load an existing rule providing a wrong but existing folder */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getSingleRuleFromWrongFolder() + { + STEP("Create a folder in existing site"); + FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + STEP("Try to load a rule for a wrong but existing folder."); + restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).usingDefaultRuleSet().getSingleRule(createdRuleA.getId()); + restClient.assertStatusCodeIs(NOT_FOUND); + } + + /** Check we can get a rule by its id along with any included fields. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void getSingleRuleWithIncludedFields() + { + STEP("Load a particular rule"); + RestRuleModel rule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .include("isShared") + .getSingleRule(createdRuleA.getId()); + + rule.assertThat().field("isShared").isNotNull(); + } + + /** Check that a user without read permission cannot view the folder rules. */ + public void requireReadPermissionToGetRule() + { + STEP("Create a user and use them to create a private site containing a folder with a rule"); + UserModel privateUser = dataUser.createRandomTestUser(); + SiteModel privateSite = dataSite.usingUser(privateUser).createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingUser(privateUser).usingSite(privateSite).createFolder(); + RestRuleModel ruleModel = new RestRuleModel(); + ruleModel.setName("Private site rule"); + restClient.authenticateUser(privateUser).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + + STEP("Try to get the rule with another user"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet().getListOfRules(); + + restClient.assertLastError() + .statusCodeIs(FORBIDDEN) + .containsSummary("Cannot read from this node"); + } + + /** Check that a user with only read permission can view the folder rules. */ + public void dontRequireWritePermissionToGetRule() + { + STEP("Create a user and use them to create a private site containing a folder with a rule"); + UserModel privateUser = dataUser.createRandomTestUser(); + SiteModel privateSite = dataSite.usingUser(privateUser).createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingUser(privateUser).usingSite(privateSite).createFolder(); + RestRuleModel ruleModel = rulesUtils.createRuleModel("Private site rule"); + restClient.authenticateUser(privateUser).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + + STEP("Create a collaborator in the private site"); + UserModel collaborator = dataUser.createRandomTestUser(); + collaborator.setUserRole(SiteCollaborator); + restClient.authenticateUser(privateUser).withCoreAPI().usingSite(privateSite).addPerson(collaborator); + + STEP("Check the collaborator can view the rule"); + RestRuleModelsCollection rules = restClient.authenticateUser(collaborator).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet().getListOfRules(); + + restClient.assertStatusCodeIs(OK); + rules.assertThat().entriesListContains("name", "Private site rule"); + } + + /** + * Check we can GET Rule's actions. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void getRuleActions() + { + STEP("Create a rule with a few actions"); + final FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + final RestRuleModel ruleWithVariousActions = rulesUtils.createRuleWithVariousActions(); + final UserModel admin = dataUser.getAdminUser(); + final RestRuleModel rule = restClient.authenticateUser(admin).withPrivateAPI().usingNode(folder).usingDefaultRuleSet() + .createSingleRule(ruleWithVariousActions); + + STEP("Retrieve the created rule via the GET endpoint"); + final RestRuleModel getRuleBody = restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).usingDefaultRuleSet().getSingleRule(rule.getId()); + + STEP("Assert that actions are returned as expected from the GET endpoint"); + restClient.assertStatusCodeIs(OK); + getRuleBody.assertThat().field(ACTIONS).contains("actionDefinitionId=copy") + .assertThat().field(ACTIONS).contains("destination-folder=" + rulesUtils.getCopyDestinationFolder().getNodeRef()) + .assertThat().field(ACTIONS).contains("deep-copy=true") + .assertThat().field(ACTIONS).contains("actionDefinitionId=check-out") + .assertThat().field(ACTIONS).contains("destination-folder=" + rulesUtils.getCheckOutDestinationFolder().getNodeRef()) + .assertThat().field(ACTIONS).contains("assoc-name=cm:checkout"); + } + + /** + * Check we can GET rule's conditions. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void getRulesConditions() + { + STEP("Create a rule with several conditions"); + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + ruleModel.setConditions(rulesUtils.createVariousConditions()); + + FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + + RestRuleModel rule = restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + STEP("Retrieve the created rule via the GET endpoint"); + final RestRuleModel getRuleBody = restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).usingDefaultRuleSet().getSingleRule(rule.getId()); + + STEP("Assert that conditions are retrieved using the GET endpoint"); + restClient.assertStatusCodeIs(OK); + getRuleBody.assertThat().field(CONDITIONS).contains("comparator=ends") + .assertThat().field(CONDITIONS).contains("field=cm:creator") + .assertThat().field(CONDITIONS).contains("parameter=ski") + .assertThat().field(CONDITIONS).contains("comparator=begins") + .assertThat().field(CONDITIONS).contains("field=cm:modelVersion") + .assertThat().field(CONDITIONS).contains("parameter=1."); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/ReorderRules.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/ReorderRules.java new file mode 100644 index 0000000000..7a60dbc70e --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/ReorderRules.java @@ -0,0 +1,203 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rules; + +import static java.util.stream.Collectors.toList; + +import static org.alfresco.utility.report.log.Step.STEP; +import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.OK; + +import java.util.List; +import java.util.stream.IntStream; + +import com.google.common.collect.Lists; + +import org.alfresco.rest.RestTest; +import org.alfresco.rest.model.RestRuleModel; +import org.alfresco.rest.model.RestRuleSetModel; +import org.alfresco.utility.constants.UserRole; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +@Test (groups = { TestGroup.RULES }) +public class ReorderRules extends RestTest +{ + private UserModel user; + private SiteModel site; + + @BeforeClass (alwaysRun = true) + public void dataPreparation() + { + STEP("Create a user and site."); + user = dataUser.createRandomTestUser(); + site = dataSite.usingUser(user).createPublicRandomSite(); + } + + /** Check we can get the ordered list of rules in a rule set. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getOrderedRuleIds() + { + STEP("Create a folder containing three rules in the existing site"); + FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + List rules = createRulesInFolder(folder, user); + + STEP("Get the default rule set for the folder including the ordered rule ids"); + RestRuleSetModel ruleSet = restClient.authenticateUser(user).withPrivateAPI().usingNode(folder) + .include("ruleIds").getDefaultRuleSet(); + + List expectedRuleIds = rules.stream().map(RestRuleModel::getId).collect(toList()); + restClient.assertStatusCodeIs(OK); + ruleSet.assertThat().field("ruleIds").is(expectedRuleIds); + } + + /** Check that a user can view the order of the rules in a rule set if they only have read permission. */ + @Test + public void getRuleSetAndRuleIdsWithReadOnlyPermission() + { + STEP("Create a site owned by admin and add user as a consumer"); + SiteModel siteModel = dataSite.usingAdmin().createPrivateRandomSite(); + dataUser.addUserToSite(user, siteModel, UserRole.SiteConsumer); + + STEP("Use admin to create a folder with a rule set and three rules in it"); + FolderModel ruleFolder = dataContent.usingAdmin().usingSite(siteModel).createFolder(); + dataContent.usingAdmin().usingResource(ruleFolder).createFolder(); + List rules = createRulesInFolder(ruleFolder, dataUser.getAdminUser()); + + STEP("Get the rule set with the ordered list of rules"); + RestRuleSetModel ruleSet = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder) + .include("ruleIds").getDefaultRuleSet(); + + restClient.assertStatusCodeIs(OK); + List ruleIds = rules.stream().map(RestRuleModel::getId).collect(toList()); + ruleSet.assertThat().field("ruleIds").is(ruleIds); + } + + /** Check we can reorder the rules in a rule set. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void reorderRules() + { + STEP("Create a folder containing three rules in the existing site"); + FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + List rules = createRulesInFolder(folder, user); + + STEP("Reverse the order of the rules within the rule set"); + List reversedRuleIds = Lists.reverse(rules.stream().map(RestRuleModel::getId).collect(toList())); + RestRuleSetModel ruleSetBody = new RestRuleSetModel(); + ruleSetBody.setId("-default-"); + ruleSetBody.setRuleIds(reversedRuleIds); + RestRuleSetModel ruleSet = restClient.authenticateUser(user).withPrivateAPI().usingNode(folder) + .include("ruleIds").updateRuleSet(ruleSetBody); + + restClient.assertStatusCodeIs(OK); + ruleSet.assertThat().field("ruleIds").is(reversedRuleIds); + } + + /** Check we can reorder the rules in a rule set by editing the response from the GET. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void reorderRulesUsingResponseFromGET() + { + STEP("Create a folder containing three rules in the existing site"); + FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + List rules = createRulesInFolder(folder, user); + + STEP("Get the rule set with its id."); + RestRuleSetModel ruleSetResponse = restClient.authenticateUser(user).withPrivateAPI().usingNode(folder) + .include("ruleIds").getDefaultRuleSet(); + + STEP("Reverse the order of the rules within the rule set"); + ruleSetResponse.setRuleIds(Lists.reverse(ruleSetResponse.getRuleIds())); + RestRuleSetModel ruleSet = restClient.authenticateUser(user).withPrivateAPI().usingNode(folder) + .include("ruleIds").updateRuleSet(ruleSetResponse); + + restClient.assertStatusCodeIs(OK); + List reversedRuleIds = Lists.reverse(rules.stream().map(RestRuleModel::getId).collect(toList())); + ruleSet.assertThat().field("ruleIds").is(reversedRuleIds); + } + + /** Check that a user cannot reorder the rules in a rule set if they only have read permission. */ + @Test + public void reorderRulesWithoutPermission() + { + STEP("Create a site owned by admin and add user as a consumer"); + SiteModel siteModel = dataSite.usingAdmin().createPrivateRandomSite(); + dataUser.addUserToSite(user, siteModel, UserRole.SiteContributor); + + STEP("Use admin to create a folder with a rule set and three rules in it"); + FolderModel ruleFolder = dataContent.usingAdmin().usingSite(siteModel).createFolder(); + dataContent.usingAdmin().usingResource(ruleFolder).createFolder(); + List rules = createRulesInFolder(ruleFolder, dataUser.getAdminUser()); + + STEP("Try to reorder the rules as the contributor"); + List reversedRuleIds = Lists.reverse(rules.stream().map(RestRuleModel::getId).collect(toList())); + RestRuleSetModel ruleSetBody = new RestRuleSetModel(); + ruleSetBody.setId("-default-"); + ruleSetBody.setRuleIds(reversedRuleIds); + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder) + .include("ruleIds").updateRuleSet(ruleSetBody); + + restClient.assertStatusCodeIs(FORBIDDEN); + } + + /** Check that a user can reorder the rules in a rule set if they have write permission. */ + @Test + public void reorderRulesWithPermission() + { + STEP("Create a site owned by admin and add user as a collaborator"); + SiteModel siteModel = dataSite.usingAdmin().createPrivateRandomSite(); + dataUser.addUserToSite(user, siteModel, UserRole.SiteCollaborator); + + STEP("Use admin to create a folder with a rule set and three rules in it"); + FolderModel ruleFolder = dataContent.usingAdmin().usingSite(siteModel).createFolder(); + dataContent.usingAdmin().usingResource(ruleFolder).createFolder(); + List rules = createRulesInFolder(ruleFolder, dataUser.getAdminUser()); + + STEP("Try to reorder the rules as the contributor"); + List reversedRuleIds = Lists.reverse(rules.stream().map(RestRuleModel::getId).collect(toList())); + RestRuleSetModel ruleSetBody = new RestRuleSetModel(); + ruleSetBody.setId("-default-"); + ruleSetBody.setRuleIds(reversedRuleIds); + RestRuleSetModel ruleSet = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder) + .include("ruleIds").updateRuleSet(ruleSetBody); + + restClient.assertStatusCodeIs(OK); + ruleSet.assertThat().field("ruleIds").is(reversedRuleIds); + } + + /** Create three rules in the given folder. */ + private List createRulesInFolder(FolderModel folder, UserModel user) + { + return IntStream.range(0, 3).mapToObj(index -> + { + RestRuleModel ruleModel = rulesUtils.createRuleModelWithDefaultValues(); + return restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).usingDefaultRuleSet().createSingleRule(ruleModel); + }).collect(toList()); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/RuleSetLinksTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/RuleSetLinksTests.java new file mode 100644 index 0000000000..844dfda7ee --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/RuleSetLinksTests.java @@ -0,0 +1,502 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rules; + +import static org.alfresco.utility.constants.UserRole.SiteConsumer; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static org.springframework.http.HttpStatus.OK; + +import org.alfresco.dataprep.CMISUtil; +import org.alfresco.rest.RestTest; +import org.alfresco.rest.model.RestRuleModel; +import org.alfresco.rest.model.RestRuleModelsCollection; +import org.alfresco.rest.model.RestRuleSetLinkModel; +import org.alfresco.rest.model.RestRuleSetModel; +import org.alfresco.rest.model.RestRuleSetModelsCollection; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Tests for /nodes/{nodeId}/rule-set-links. + */ +@Test(groups = {TestGroup.RULES}) +public class RuleSetLinksTests extends RestTest +{ + private UserModel user; + private SiteModel site; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + STEP("Create a user and site."); + user = dataUser.createRandomTestUser(); + site = dataSite.usingUser(user).createPublicRandomSite(); + } + + /** + * Check we can link to folder containing a rule set. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void linkToFolderContainingRules() + { + STEP("Create folders in existing site"); + final FolderModel ruleFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + final FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + + STEP("Create a rule in the rule folder."); + RestRuleModel ruleModel = rulesUtils.createRuleModel("ruleName"); + RestRuleModel rule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + STEP("Get the rule sets for the folder and find the rule set id"); + final RestRuleSetModelsCollection ruleSets = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder) + .getListOfRuleSets(); + ruleSets.assertThat().entriesListCountIs(1); + final String ruleSetId = ruleSets.getEntries().get(0).onModel().getId(); + + STEP("Link to a rule folder"); + final RestRuleSetLinkModel request = new RestRuleSetLinkModel(); + request.setId(ruleFolder.getNodeRef()); + final RestRuleSetLinkModel ruleLink = restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).createRuleLink(request); + + STEP("Assert link result"); + restClient.assertStatusCodeIs(CREATED); + final RestRuleSetLinkModel expectedLink = new RestRuleSetLinkModel(); + expectedLink.setId(ruleSetId); + ruleLink.assertThat().isEqualTo(expectedLink); + + STEP("Check if folder returns same rules"); + final RestRuleModelsCollection linkedRules = restClient.authenticateUser(user).withPrivateAPI() + .usingNode(folder) + .usingDefaultRuleSet() + .getListOfRules(); + linkedRules.assertThat().entriesListCountIs(1); + linkedRules.getEntries().get(0).onModel().assertThat().isEqualTo(rule); + + STEP("Check if folder returns rule set with linked inclusionType"); + final RestRuleSetModelsCollection linkedRuleSets = restClient.authenticateUser(user).withPrivateAPI() + .usingNode(folder) + .include("inclusionType") + .getListOfRuleSets(); + linkedRuleSets.assertThat().entriesListCountIs(1); + final RestRuleSetModel expectedRuleSet = new RestRuleSetModel(); + expectedRuleSet.setId(ruleSetId); + expectedRuleSet.setInclusionType("linked"); + linkedRuleSets.getEntries() + .get(0).onModel().assertThat().isEqualTo(expectedRuleSet); + } + + /** + * Check we can link to a rule set. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void linkToRuleSet() + { + STEP("Create folders in existing site"); + final FolderModel ruleFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + final FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + + STEP("Create a rule in the rule folder."); + RestRuleModel ruleModel = rulesUtils.createRuleModel("ruleName"); + RestRuleModel rule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + STEP("Get the rule sets for the folder and find the rule set id"); + final RestRuleSetModelsCollection ruleSets = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder) + .getListOfRuleSets(); + ruleSets.assertThat().entriesListCountIs(1); + final String ruleSetId = ruleSets.getEntries().get(0).onModel().getId(); + + STEP("Link to a rule set"); + final RestRuleSetLinkModel request = new RestRuleSetLinkModel(); + request.setId(ruleSetId); + final RestRuleSetLinkModel ruleLink = restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).createRuleLink(request); + + STEP("Assert link result"); + restClient.assertStatusCodeIs(CREATED); + final RestRuleSetLinkModel expectedLink = new RestRuleSetLinkModel(); + expectedLink.setId(ruleSetId); + ruleLink.assertThat().isEqualTo(expectedLink); + + STEP("Check if folder returns same rules"); + final RestRuleModelsCollection linkedRules = restClient.authenticateUser(user).withPrivateAPI() + .usingNode(folder) + .usingDefaultRuleSet() + .getListOfRules(); + linkedRules.assertThat().entriesListCountIs(1); + linkedRules.getEntries().get(0).onModel().assertThat().isEqualTo(rule); + + STEP("Check if folder returns rule set with linked inclusionType"); + final RestRuleSetModelsCollection likedRuleSets = restClient.authenticateUser(user).withPrivateAPI() + .usingNode(folder) + .include("inclusionType") + .getListOfRuleSets(); + likedRuleSets.assertThat().entriesListCountIs(1); + final RestRuleSetModel expectedRuleSet = new RestRuleSetModel(); + expectedRuleSet.setId(ruleSetId); + expectedRuleSet.setInclusionType("linked"); + likedRuleSets.getEntries() + .get(0).onModel().assertThat().isEqualTo(expectedRuleSet); + } + + /** + * Check we get 404 when linking to a non-existing rule set/folder. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void linkToNonExistingRuleSet() + { + STEP("Create a folder in existing site"); + final FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + + STEP("Link to non-existing rule set"); + final RestRuleSetLinkModel request = new RestRuleSetLinkModel(); + request.setId("dummy-rule-set-id"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).createRuleLink(request); + + STEP("Assert link result is 404"); + restClient.assertStatusCodeIs(NOT_FOUND); + } + + /** + * Check we get bad request error when linking to a folder without rules. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void linkToFolderWithoutRules() + { + STEP("Create 2 folders without rules in existing site"); + final FolderModel folder1 = dataContent.usingUser(user).usingSite(site).createFolder(); + final FolderModel folder2 = dataContent.usingUser(user).usingSite(site).createFolder(); + + STEP("Link to a folder without rules"); + final RestRuleSetLinkModel request = new RestRuleSetLinkModel(); + request.setId(folder2.getNodeRef()); + restClient.authenticateUser(user).withPrivateAPI().usingNode(folder1).createRuleLink(request); + + STEP("Assert link result is 400"); + restClient.assertStatusCodeIs(BAD_REQUEST) + .assertLastError().containsSummary("The target node has no rules to link."); + } + + /** + * Check we get bad request error when linking from a folder which already has rules. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void linkFromFolderWithRules() + { + STEP("Create folders in existing site"); + final FolderModel folder1 = dataContent.usingUser(user).usingSite(site).createFolder(); + final FolderModel folder2 = dataContent.usingUser(user).usingSite(site).createFolder(); + + STEP("Create rules in both folders."); + RestRuleModel ruleModel1 = rulesUtils.createRuleModel("ruleName1"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(folder1).usingDefaultRuleSet() + .createSingleRule(ruleModel1); + RestRuleModel ruleModel2 = rulesUtils.createRuleModel("ruleName2"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(folder2).usingDefaultRuleSet() + .createSingleRule(ruleModel2); + + STEP("Link from a folder with rules"); + final RestRuleSetLinkModel request = new RestRuleSetLinkModel(); + request.setId(folder2.getNodeRef()); + restClient.authenticateUser(user).withPrivateAPI().usingNode(folder1).createRuleLink(request); + + STEP("Assert link result is 400"); + restClient.assertStatusCodeIs(BAD_REQUEST) + .assertLastError().containsSummary( + "Unable to link to a rule set because the folder has pre-existing rules or is already linked to a rule set."); + } + + /** + * Check we get bad request error when linking to a file node. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void linkToFileNode() + { + STEP("Create a folder in existing site"); + final FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + final FileModel fileContent = dataContent.usingUser(user).usingSite(site).createContent(CMISUtil.DocumentType.TEXT_PLAIN); + + STEP("Link to a file node"); + final RestRuleSetLinkModel request = new RestRuleSetLinkModel(); + request.setId(fileContent.getNodeRef()); + restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).createRuleLink(request); + + STEP("Assert link result is 400"); + restClient.assertStatusCodeIs(BAD_REQUEST) + .assertLastError().containsSummary("NodeId of a folder is expected!"); + } + + /** + * Check we can link to a parent folder with rules. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void linkToParentNodeWithRules() + { + STEP("Create parent/child folders in existing site"); + final FolderModel parentFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + final FolderModel childFolder = dataContent.usingUser(user).usingSite(site).usingResource(parentFolder).createFolder(); + + STEP("Create a rule in the parent folder."); + RestRuleModel ruleModel = rulesUtils.createRuleModel("ruleName"); + RestRuleModel rule = restClient.authenticateUser(user).withPrivateAPI().usingNode(parentFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + STEP("Get the rule sets for the folder and find the rule set id"); + final RestRuleSetModelsCollection ruleSets = restClient.authenticateUser(user).withPrivateAPI().usingNode(parentFolder) + .getListOfRuleSets(); + ruleSets.assertThat().entriesListCountIs(1); + final String ruleSetId = ruleSets.getEntries().get(0).onModel().getId(); + + STEP("Link to the parent folder"); + final RestRuleSetLinkModel request = new RestRuleSetLinkModel(); + request.setId(parentFolder.getNodeRef()); + final RestRuleSetLinkModel ruleLink = restClient.authenticateUser(user).withPrivateAPI().usingNode(childFolder).createRuleLink(request); + + STEP("Assert link result"); + restClient.assertStatusCodeIs(CREATED); + final RestRuleSetLinkModel expectedLink = new RestRuleSetLinkModel(); + expectedLink.setId(ruleSetId); + ruleLink.assertThat().isEqualTo(expectedLink); + + STEP("Check if child folder returns same rules"); + final RestRuleModelsCollection linkedRules = restClient.authenticateUser(user).withPrivateAPI() + .usingNode(childFolder) + .usingDefaultRuleSet() + .getListOfRules(); + linkedRules.assertThat().entriesListCountIs(1); + linkedRules.getEntries().get(0).onModel().assertThat().isEqualTo(rule); + + STEP("Check if child folder returns rule set with linked inclusionType"); + final RestRuleSetModelsCollection linkedRuleSets = restClient.authenticateUser(user).withPrivateAPI() + .usingNode(childFolder) + .include("inclusionType") + .getListOfRuleSets(); + linkedRuleSets.assertThat().entriesListCountIs(1); + final RestRuleSetModel expectedRuleSet = new RestRuleSetModel(); + expectedRuleSet.setId(ruleSetId); + expectedRuleSet.setInclusionType("linked"); + linkedRuleSets.getEntries() + .get(0).onModel().assertThat().isEqualTo(expectedRuleSet); + } + + /** + * Check we get an error when trying to link to a rule set that we can't view. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void linkToRuleSetWithoutPermission() + { + STEP("Use admin to create a private site with a folder containing a rule."); + SiteModel privateSite = dataSite.usingAdmin().createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingAdmin().usingSite(privateSite).createFolder(); + restClient.authenticateUser(dataUser.getAdminUser()).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet() + .createSingleRule(rulesUtils.createRuleModelWithDefaultValues()); + + STEP("Use a normal user to try to link to the rule."); + FolderModel publicFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + RestRuleSetLinkModel request = new RestRuleSetLinkModel(); + request.setId(privateFolder.getNodeRef()); + restClient.authenticateUser(user).withPrivateAPI().usingNode(publicFolder).createRuleLink(request); + + restClient.assertStatusCodeIs(FORBIDDEN); + } + + /** + * Check we are able to link to a rule set with only read permission. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void linkToRuleSetWithOnlyReadPermission() + { + STEP("Use admin to create a private site with a folder containing a rule."); + SiteModel privateSite = dataSite.usingAdmin().createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingAdmin().usingSite(privateSite).createFolder(); + restClient.authenticateUser(dataUser.getAdminUser()).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet() + .createSingleRule(rulesUtils.createRuleModelWithDefaultValues()); + + STEP("Add the normal user as a consumer."); + dataUser.usingAdmin().addUserToSite(user, privateSite, SiteConsumer); + + STEP("Use a normal user to try to link to the rule."); + FolderModel publicFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + RestRuleSetLinkModel request = new RestRuleSetLinkModel(); + request.setId(privateFolder.getNodeRef()); + restClient.authenticateUser(user).withPrivateAPI().usingNode(publicFolder).createRuleLink(request); + + restClient.assertStatusCodeIs(CREATED); + } + + /** + * Check we can DELETE/unlink a ruleset + * + * DELETE /nodes/{folderNodeId}/rule-set-links/{ruleSetId}. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void unlinkRuleSet() + { + STEP("Create folders in existing site"); + final FolderModel ruleFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + final FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + + STEP("Create a rule in the rule folder."); + RestRuleModel ruleModel = rulesUtils.createRuleModel("ruleName"); + RestRuleModel rule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + + STEP("Get the rule sets for the folder and find the rule set id"); + final RestRuleSetModelsCollection ruleSets = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder) + .getListOfRuleSets(); + ruleSets.assertThat().entriesListCountIs(1); + final String ruleSetId = ruleSets.getEntries().get(0).onModel().getId(); + + STEP("Link to a rule folder"); + final RestRuleSetLinkModel request = new RestRuleSetLinkModel(); + request.setId(ruleFolder.getNodeRef()); + final RestRuleSetLinkModel ruleLink = restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).createRuleLink(request); + + STEP("Unlink the rule set"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).unlinkRuleSet(ruleSetId); + + STEP("Assert unlink result"); + restClient.assertStatusCodeIs(NO_CONTENT); + + STEP("GET the rule set and isLinkedTo field."); + RestRuleSetModel ruleSet = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder) + .include("isLinkedTo", "linkedToBy", "owningFolder") + .getDefaultRuleSet(); + + STEP("Assert linkedTo is false."); + restClient.assertStatusCodeIs(OK); + ruleSet.assertThat().field("isLinkedTo").is(false) + .assertThat().field("linkedToBy").isEmpty(); + } + + /** + * Check a 400 is thrown when using folder/content id instead of a ruleSetId. + * + * DELETE /nodes/{folderNodeId}/rule-set-links/{ruleSetId} + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void unlinkUsingDocumentId() + { + STEP("Create folders in existing site"); + final FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + + STEP("Attempt to unlink the rule set"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).unlinkRuleSet(folder.getNodeRef()); + + STEP("Assert unlink result"); + restClient.assertStatusCodeIs(BAD_REQUEST) + .assertLastError().containsSummary("NodeId of a rule set is expected!"); + } + + /** + * Check a 404 is thrown when using non-existent id instead of a ruleSetId. + * + * DELETE /nodes/{folderNodeId}/rule-set-links/{ruleSetId} + */ + //TODO This test may need to be modified once ACS-3616 is done + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void unlinkUsingRandomId() + { + STEP("Create folders in existing site"); + final FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + + STEP("Attempt to unlink the rule set"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(folder).unlinkRuleSet("non-existent-id"); + + STEP("Assert unlink result"); + restClient.assertStatusCodeIs(NOT_FOUND) + .assertLastError().containsSummary("Rule set with id non-existent-id was not found"); + } + + /** + * Check we cannot unlink from a rule set that we can't view. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void unlinkFromRuleSetWithoutPermission() + { + STEP("Use admin to create a private site with a folder containing a rule."); + SiteModel privateSite = dataSite.usingAdmin().createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingAdmin().usingSite(privateSite).createFolder(); + restClient.authenticateUser(dataUser.getAdminUser()).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet() + .createSingleRule(rulesUtils.createRuleModelWithDefaultValues()); + + STEP("Add the user as a consumer."); + dataUser.usingAdmin().addUserToSite(user, privateSite, SiteConsumer); + + STEP("Use the consumer to create a folder with a link to the private rule set."); + FolderModel publicFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + RestRuleSetLinkModel request = new RestRuleSetLinkModel(); + request.setId(privateFolder.getNodeRef()); + restClient.authenticateUser(user).withPrivateAPI().usingNode(publicFolder).createRuleLink(request); + restClient.assertStatusCodeIs(CREATED); + + STEP("Remove the user from the private site."); + dataUser.usingAdmin().removeUserFromSite(user, privateSite); + + STEP("Use the user to try to unlink from the rule set."); + restClient.authenticateUser(user).withPrivateAPI().usingNode(publicFolder).unlinkRuleSet("-default-"); + + restClient.assertStatusCodeIs(FORBIDDEN); + } + + /** + * Check we can unlink from a rule set if we only have read permission for it. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void unlinkFromRuleSetWithOnlyReadPermission() + { + STEP("Use admin to create a private site with a folder containing a rule."); + SiteModel privateSite = dataSite.usingAdmin().createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingAdmin().usingSite(privateSite).createFolder(); + restClient.authenticateUser(dataUser.getAdminUser()).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet() + .createSingleRule(rulesUtils.createRuleModelWithDefaultValues()); + + STEP("Add the user as a consumer."); + dataUser.usingAdmin().addUserToSite(user, privateSite, SiteConsumer); + + STEP("Use the consumer to create a folder with a link to the private rule set."); + FolderModel publicFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + RestRuleSetLinkModel request = new RestRuleSetLinkModel(); + request.setId(privateFolder.getNodeRef()); + restClient.authenticateUser(user).withPrivateAPI().usingNode(publicFolder).createRuleLink(request); + restClient.assertStatusCodeIs(CREATED); + + STEP("Use the consumer to try to unlink from the rule set."); + restClient.authenticateUser(user).withPrivateAPI().usingNode(publicFolder).unlinkRuleSet("-default-"); + + restClient.assertStatusCodeIs(NO_CONTENT); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/RulesTestsUtils.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/RulesTestsUtils.java new file mode 100644 index 0000000000..d0372f9a18 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/RulesTestsUtils.java @@ -0,0 +1,385 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rules; + +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.MAIL_ACTION; +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.createMailParameters; +import static org.alfresco.utility.model.UserModel.getRandomUserModel; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.alfresco.rest.core.RestWrapper; +import org.alfresco.rest.model.RestActionBodyExecTemplateModel; +import org.alfresco.rest.model.RestActionConstraintDataModel; +import org.alfresco.rest.model.RestActionConstraintModel; +import org.alfresco.rest.model.RestActionDefinitionModel; +import org.alfresco.rest.model.RestCompositeConditionDefinitionModel; +import org.alfresco.rest.model.RestNodeModel; +import org.alfresco.rest.model.RestParameterDefinitionModel; +import org.alfresco.rest.model.RestRuleExecutionModel; +import org.alfresco.rest.model.RestRuleModel; +import org.alfresco.rest.model.RestSimpleConditionDefinitionModel; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.data.DataUserAIS; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.UserModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class RulesTestsUtils +{ + static final String RULE_NAME_DEFAULT = "ruleName"; + static final String RULE_DESCRIPTION_DEFAULT = "rule description"; + static final boolean RULE_ENABLED_DEFAULT = true; + static final boolean RULE_CASCADE_DEFAULT = true; + static final boolean RULE_ASYNC_DEFAULT = true; + static final boolean RULE_SHARED_DEFAULT = false; + static final String RULE_ERROR_SCRIPT_LABEL = "Start Pooled Review and Approve Workflow"; + static final String INBOUND = "inbound"; + static final String UPDATE = "update"; + static final String OUTBOUND = "outbound"; + static final List RULE_TRIGGERS_DEFAULT = List.of(INBOUND, UPDATE, OUTBOUND); + static final boolean INVERTED = true; + static final String AND = "and"; + static final String ID = "id"; + static final String IS_SHARED = "isShared"; + static final String AUDIO_ASPECT = "audio:audio"; + static final String LOCKABLE_ASPECT = "cm:lockable"; + static final String TEMPLATE_PARAM = "template"; + static final String RULE_SCRIPT_PARAM_ID = "script-ref"; + static final String CHECKIN_ACTION = "check-in"; + static final String LINK_CATEGORY_ACTION = "link-category"; + static final String DELETE_RENDITION_ACTION = "delete-rendition"; + static final String COPY_ACTION = "copy"; + static final String ADD_FEATURES_ACTION = "add-features"; + static final String MOVE_ACTION = "move"; + static final String SCRIPT_ACTION = "script"; + + @Autowired + private RestWrapper restClient; + @Autowired + private DataUserAIS dataUser; + @Autowired + private DataSite dataSite; + @Autowired + private DataContent dataContent; + + /** Public site used by these helper methods. This is populated by the getter and should not be accessed directly. */ + private SiteModel publicSite; + /** Script node used by these helper methods. This is populated by the getter and should not be accessed directly. */ + private String reviewAndApproveWorkflowNode; + /** Destination folder for copy action used by these helper methods. This is populated by the getter and should not be accessed directly. */ + private FolderModel copyDestinationFolder; + /** Destination folder for check out action used by these helper methods. This is populated by the getter and should not be accessed directly. */ + private FolderModel checkOutDestinationFolder; + + /** + * Get the constraint value for a given action parameter label. + * + * @param user The user to use to obtain the information. + * @param actionId The id of the action definition. + * @param paramId The id of the parameter for the action. + * @param constraintLabel The label of the desired value of the parameter. + * @return The value to use for the parameter. + */ + public String findConstraintValue(UserModel user, String actionId, String paramId, String constraintLabel) + { + RestActionConstraintModel constraintDef = getConstraintsForActionParam(user, actionId, paramId); + RestActionConstraintDataModel constraintDataModel = constraintDef.getConstraintValues().stream().filter(constraintValue -> constraintValue.getLabel().equals(constraintLabel)).findFirst().get(); + return constraintDataModel.getValue(); + } + + /** + * Get all constraint values for a given action parameter. + * + * @param user The user to use to obtain the information. + * @param actionId The id of the action definition. + * @param paramId The id of the parameter for the action. + * @return The value to use for the parameter. + */ + public RestActionConstraintModel getConstraintsForActionParam(UserModel user, String actionId, String paramId) + { + RestActionDefinitionModel actionDef = restClient.authenticateUser(user).withCoreAPI().usingActions().getActionDefinitionById(actionId); + RestParameterDefinitionModel paramDef = actionDef.getParameterDefinitions().stream().filter(param -> param.getName().equals(paramId)).findFirst().get(); + if (paramDef.getParameterConstraintName() == null) + { + throw new IllegalArgumentException("Supplied parameter " + paramId + " for action " + actionId + " does not have a defined constraint."); + } + String constraintName = paramDef.getParameterConstraintName(); + return restClient.authenticateUser(user).withCoreAPI().usingActions().getActionConstraintByName(constraintName); + } + + /** + * Get the review and approve workflow node (throwing an exception if this utility class has not been initialised). + * + * @return The node ref of the script node. + */ + public String getReviewAndApproveWorkflowNode() + { + if (reviewAndApproveWorkflowNode == null) + { + UserModel admin = dataUser.getAdminUser(); + reviewAndApproveWorkflowNode = findConstraintValue(admin, SCRIPT_ACTION, RULE_SCRIPT_PARAM_ID, RULE_ERROR_SCRIPT_LABEL); + } + return reviewAndApproveWorkflowNode; + } + + public SiteModel getPublicSite() + { + if (publicSite == null) + { + UserModel admin = dataUser.getAdminUser(); + publicSite = dataSite.usingUser(admin).createPublicRandomSite(); + } + return publicSite; + } + + public FolderModel getCopyDestinationFolder() + { + if (copyDestinationFolder == null) + { + UserModel admin = dataUser.getAdminUser(); + copyDestinationFolder = dataContent.usingUser(admin).usingSite(getPublicSite()).createFolder(); + } + return copyDestinationFolder; + } + + public FolderModel getCheckOutDestinationFolder() + { + if (checkOutDestinationFolder == null) + { + UserModel admin = dataUser.getAdminUser(); + checkOutDestinationFolder = dataContent.usingUser(admin).usingSite(getPublicSite()).createFolder(); + } + return checkOutDestinationFolder; + } + + public RestRuleModel createRuleModelWithModifiedValues() + { + return createRuleModelWithModifiedValues(List.of(createAddAudioAspectAction())); + } + + /** + * Create a rule model filled with custom constant values. + * + * @param actions - rule's actions. + * @return The created rule model. + */ + public RestRuleModel createRuleModelWithModifiedValues(List actions) + { + RestRuleModel ruleModel = createRuleModel(RULE_NAME_DEFAULT, actions); + ruleModel.setDescription(RULE_DESCRIPTION_DEFAULT); + ruleModel.setIsEnabled(RULE_ENABLED_DEFAULT); + ruleModel.setIsInheritable(RULE_CASCADE_DEFAULT); + ruleModel.setIsAsynchronous(RULE_ASYNC_DEFAULT); + ruleModel.setIsShared(RULE_SHARED_DEFAULT); + ruleModel.setTriggers(RULE_TRIGGERS_DEFAULT); + ruleModel.setErrorScript(getReviewAndApproveWorkflowNode()); + + return ruleModel; + } + + public RestRuleModel createRuleModelWithDefaultValues() + { + return createRuleModel(RULE_NAME_DEFAULT); + } + + public RestRuleModel createRuleModel(String name) + { + return createRuleModel(name, List.of(createAddAudioAspectAction())); + } + + /** + * Create a rule model. + * + * @param name The name for the rule. + * @param actions Rule's actions. + * @return The created rule model. + */ + public RestRuleModel createRuleModel(String name, List actions) + { + RestRuleModel ruleModel = new RestRuleModel(); + ruleModel.setIsEnabled(true); + ruleModel.setName(name); + ruleModel.setActions(actions); + return ruleModel; + } + + /** + * Create a rule's action model. + * + * @return The created action model. + */ + public RestActionBodyExecTemplateModel createAddAudioAspectAction() + { + return createAddAspectAction(AUDIO_ASPECT); + } + + public RestActionBodyExecTemplateModel createAddAspectAction(String aspect) + { + return createCustomActionModel(ADD_FEATURES_ACTION, Map.of("aspect-name", aspect)); + } + + public RestActionBodyExecTemplateModel createCustomActionModel(String actionDefinitionId, Map params) + { + RestActionBodyExecTemplateModel restActionModel = new RestActionBodyExecTemplateModel(); + restActionModel.setActionDefinitionId(actionDefinitionId); + restActionModel.setParams(params); + return restActionModel; + } + + public RestCompositeConditionDefinitionModel createEmptyConditionModel() + { + RestCompositeConditionDefinitionModel conditions = new RestCompositeConditionDefinitionModel(); + conditions.setInverted(!INVERTED); + conditions.setBooleanMode(AND); + return conditions; + } + + public RestCompositeConditionDefinitionModel createVariousConditions() + { + return createCompositeCondition(List.of( + createCompositeCondition(!INVERTED, List.of( + createSimpleCondition("cm:created", "less_than", "2022-09-01T12:59:00.000+02:00"), + createSimpleCondition("cm:creator", "ends", "ski"), + createSimpleCondition("size", "greater_than", "90000000"), + createSimpleCondition("mimetype", "equals", "video/3gpp"), + createSimpleCondition("encoding", "equals", "utf-8"), + createSimpleCondition("type", "equals", "cm:folder"), + createSimpleCondition("tag", "equals", "uat") + )), + createCompositeCondition(INVERTED, List.of( + createSimpleCondition("aspect", "equals", AUDIO_ASPECT), + createSimpleCondition("cm:modelVersion", "begins", "1.") + )) + )); + } + + public RestRuleModel createRuleWithVariousActions() + { + final Map copyParams = + Map.of("destination-folder", getCopyDestinationFolder().getNodeRef(), "deep-copy", true); + final RestActionBodyExecTemplateModel copyAction = createCustomActionModel(COPY_ACTION, copyParams); + final Map checkOutParams = + Map.of("destination-folder", getCheckOutDestinationFolder().getNodeRef(), "assoc-name", "cm:checkout", + "assoc-type", "cm:contains"); + final RestActionBodyExecTemplateModel checkOutAction = createCustomActionModel("check-out", checkOutParams); + // The counter action takes no parameters, so check we can omit the "params" entry. + final RestActionBodyExecTemplateModel counterAction = createCustomActionModel("counter", null); + final RestRuleModel ruleModel = createRuleModelWithDefaultValues(); + ruleModel.setActions(Arrays.asList(copyAction, checkOutAction, counterAction)); + + return ruleModel; + } + + public RestRuleModel createRuleWithPrivateAction() + { + RestActionBodyExecTemplateModel mailAction = new RestActionBodyExecTemplateModel(); + mailAction.setActionDefinitionId(MAIL_ACTION); + mailAction.setParams(createMailParameters(getRandomUserModel(), getRandomUserModel())); + RestRuleModel ruleModel = createRuleModelWithDefaultValues(); + ruleModel.setActions(Arrays.asList(mailAction)); + return ruleModel; + } + + public RestSimpleConditionDefinitionModel createSimpleCondition(String field, String comparator, String parameter) + { + RestSimpleConditionDefinitionModel simpleCondition = new RestSimpleConditionDefinitionModel(); + simpleCondition.setField(field); + simpleCondition.setComparator(comparator); + simpleCondition.setParameter(parameter); + return simpleCondition; + } + + public RestCompositeConditionDefinitionModel createCompositeCondition(List compositeConditions) + { + return createCompositeCondition(AND, !INVERTED, compositeConditions, null); + } + + public RestCompositeConditionDefinitionModel createCompositeCondition(boolean inverted, + List simpleConditions) + { + return createCompositeCondition(AND, inverted, null, simpleConditions); + } + + public RestRuleExecutionModel createRuleExecutionRequest() + { + return createRuleExecutionRequest(false); + } + + public RestRuleExecutionModel createRuleExecutionRequest(boolean eachSubFolderIncluded) + { + RestRuleExecutionModel ruleExecutionBody = new RestRuleExecutionModel(); + ruleExecutionBody.setIsEachSubFolderIncluded(eachSubFolderIncluded); + + return ruleExecutionBody; + } + + private RestCompositeConditionDefinitionModel createCompositeCondition(String booleanMode, boolean inverted, + List compositeConditions, List simpleConditions) + { + RestCompositeConditionDefinitionModel compositeCondition = new RestCompositeConditionDefinitionModel(); + compositeCondition.setBooleanMode(booleanMode); + compositeCondition.setInverted(inverted); + compositeCondition.setCompositeConditions(compositeConditions); + compositeCondition.setSimpleConditions(simpleConditions); + + return compositeCondition; + } + + public NodeAssertion assertThat(RestNodeModel node) + { + return new NodeAssertion(node); + } + + public class NodeAssertion + { + private final RestNodeModel node; + + private NodeAssertion(RestNodeModel node) + { + this.node = node; + } + + public NodeAssertion containsAspects(String ...expectedAspects) + { + Arrays.stream(expectedAspects).forEach(aspect -> node.assertThat().field("aspectNames").contains(aspect)); + return this; + } + + public NodeAssertion notContainsAspects(String ...unexpectedAspects) + { + Arrays.stream(unexpectedAspects).forEach(aspect -> node.assertThat().field("aspectNames").notContains(aspect)); + return this; + } + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/SetInheritanceTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/SetInheritanceTests.java new file mode 100644 index 0000000000..aeee780598 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/SetInheritanceTests.java @@ -0,0 +1,232 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rules; + +import static org.alfresco.rest.requests.RuleSettings.IS_INHERITANCE_ENABLED; +import static org.alfresco.utility.constants.UserRole.SiteCollaborator; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.OK; + +import org.alfresco.rest.RestTest; +import org.alfresco.rest.model.RestRuleSettingsModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Tests for GET and PUT /nodes/{nodeId}/rule-settings/{ruleSettingKey}. + */ +@Test (groups = { TestGroup.RULES }) +public class SetInheritanceTests extends RestTest +{ + private UserModel siteOwner; + private SiteModel site; + + @BeforeClass (alwaysRun = true) + public void dataPreparation() + { + STEP("Create a user, site and folder."); + siteOwner = dataUser.createRandomTestUser(); + site = dataSite.usingUser(siteOwner).createPrivateRandomSite(); + } + + /** Check we can get the -isInheritanceEnabled- rule setting for the folder. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getIsInherited() + { + STEP("Create a folder for the test."); + FolderModel folder = dataContent.usingUser(siteOwner).usingSite(site).createFolder(); + + STEP("Get the -isInheritanceEnabled- rule settings for the folder."); + RestRuleSettingsModel ruleSettingsModel = restClient.authenticateUser(siteOwner) + .withPrivateAPI() + .usingNode(folder) + .usingIsInheritanceEnabledRuleSetting() + .retrieveSetting(); + + restClient.assertStatusCodeIs(OK); + RestRuleSettingsModel expected = new RestRuleSettingsModel(); + expected.setKey(IS_INHERITANCE_ENABLED); + expected.setValue(true); + ruleSettingsModel.assertThat().isEqualTo(expected); + } + + /** Check we get an error when trying to get settings from a non-existent folder. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getIsInheritedFromNonExistentFolder() + { + STEP("Try to get the -isInheritanceEnabled- rule settings for a fake folder."); + FolderModel nonExistentFolder = FolderModel.getRandomFolderModel(); + nonExistentFolder.setNodeRef("fake-id"); + restClient.authenticateUser(siteOwner) + .withPrivateAPI() + .usingNode(nonExistentFolder) + .usingIsInheritanceEnabledRuleSetting() + .retrieveSetting(); + + restClient.assertLastError().statusCodeIs(NOT_FOUND) + .containsSummary("Folder with id fake-id was not found"); + } + + /** Check we get an error when trying to retrieve a non-existent setting. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getNonExistentSetting() + { + STEP("Create a folder for the test."); + FolderModel folder = dataContent.usingUser(siteOwner).usingSite(site).createFolder(); + + STEP("Try to get a fake setting from the folder."); + restClient.authenticateUser(siteOwner).withPrivateAPI().usingNode(folder).usingRuleSetting("-fakeRuleSetting-") + .retrieveSetting(); + + restClient.assertLastError().statusCodeIs(NOT_FOUND) + .containsSummary("Unrecognised rule setting key -fakeRuleSetting-"); + } + + /** Check a user without permission for the folder cannot get the -isInheritanceEnabled- rule setting. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void getIsInheritedWithoutPermission() + { + STEP("Create a folder and a user without permission to access it."); + FolderModel folder = dataContent.usingUser(siteOwner).usingSite(site).createFolder(); + UserModel noPermissionUser = dataUser.createRandomTestUser(); + + STEP("Try to get the -isInheritanceEnabled- setting without permission."); + restClient.authenticateUser(noPermissionUser) + .withPrivateAPI() + .usingNode(folder) + .usingIsInheritanceEnabledRuleSetting() + .retrieveSetting(); + + restClient.assertLastError().statusCodeIs(FORBIDDEN) + .containsSummary("Cannot read from this node"); + } + + /** Check we can change the -isInheritanceEnabled- rule setting for the folder. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void updateIsInherited() + { + STEP("Create a folder for the test."); + FolderModel folder = dataContent.usingUser(siteOwner).usingSite(site).createFolder(); + + STEP("Set -isInheritanceEnabled- to false."); + RestRuleSettingsModel updateBody = new RestRuleSettingsModel(); + updateBody.setValue(false); + + RestRuleSettingsModel ruleSettingsModel = restClient.authenticateUser(siteOwner) + .withPrivateAPI() + .usingNode(folder) + .usingIsInheritanceEnabledRuleSetting() + .updateSetting(updateBody); + + restClient.assertStatusCodeIs(OK); + RestRuleSettingsModel expected = new RestRuleSettingsModel(); + expected.setKey(IS_INHERITANCE_ENABLED); + expected.setValue(false); + ruleSettingsModel.assertThat().isEqualTo(expected); + } + + /** Check we get an error when trying to set -isInheritanceEnabled- to something other than a boolean. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void updateInheritedWithBadValue() + { + STEP("Create a folder for the test."); + FolderModel folder = dataContent.usingUser(siteOwner).usingSite(site).createFolder(); + + STEP("Try to set -isInheritanceEnabled- to \"banana\"."); + RestRuleSettingsModel updateBody = new RestRuleSettingsModel(); + updateBody.setValue("banana"); + + restClient.authenticateUser(siteOwner).withPrivateAPI().usingNode(folder).usingIsInheritanceEnabledRuleSetting() + .updateSetting(updateBody); + + restClient.assertLastError().statusCodeIs(BAD_REQUEST) + .containsSummary("Rule setting " + IS_INHERITANCE_ENABLED + " requires a boolean value."); + } + + /** Check we get an error when the folder is not found. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void updateInheritedWithNonExistentFolder() + { + STEP("Try to set -isInheritanceEnabled- against a fake folder."); + FolderModel nonExistentFolder = FolderModel.getRandomFolderModel(); + nonExistentFolder.setNodeRef("fake-id"); + + RestRuleSettingsModel updateBody = new RestRuleSettingsModel(); + updateBody.setValue(true); + + restClient.authenticateUser(siteOwner).withPrivateAPI().usingNode(nonExistentFolder).usingIsInheritanceEnabledRuleSetting() + .updateSetting(updateBody); + + restClient.assertLastError().statusCodeIs(NOT_FOUND) + .containsSummary("Folder with id fake-id was not found"); + } + + /** Check we get an error when trying to set a non-existent setting. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void updateNonExistentSetting() + { + STEP("Create a folder for the test."); + FolderModel folder = dataContent.usingUser(siteOwner).usingSite(site).createFolder(); + + STEP("Try to set a fake setting on the folder."); + RestRuleSettingsModel updateBody = new RestRuleSettingsModel(); + updateBody.setValue(true); + + restClient.authenticateUser(siteOwner).withPrivateAPI().usingNode(folder).usingRuleSetting("-fakeRuleSetting-") + .updateSetting(updateBody); + + restClient.assertLastError().statusCodeIs(NOT_FOUND) + .containsSummary("Unrecognised rule setting key -fakeRuleSetting-"); + } + + /** Check a user without manage permission cannot update the -isInheritanceEnabled- rule setting. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void updateIsInheritedWithoutPermission() + { + STEP("Create a folder and a collaborator."); + FolderModel folder = dataContent.usingUser(siteOwner).usingSite(site).createFolder(); + UserModel collaborator = dataUser.createRandomTestUser(); + collaborator.setUserRole(SiteCollaborator); + restClient.authenticateUser(siteOwner).withCoreAPI().usingSite(site).addPerson(collaborator); + + STEP("Try to update the -isInheritanceEnabled- setting without permission."); + RestRuleSettingsModel updateBody = new RestRuleSettingsModel(); + updateBody.setValue(true); + + restClient.authenticateUser(collaborator).withPrivateAPI().usingNode(folder).usingIsInheritanceEnabledRuleSetting() + .updateSetting(updateBody); + + restClient.assertLastError().statusCodeIs(FORBIDDEN) + .containsSummary("Insufficient permissions to manage rules"); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/UpdateRulesTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/UpdateRulesTests.java new file mode 100644 index 0000000000..17d55ed555 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/rules/UpdateRulesTests.java @@ -0,0 +1,592 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.rules; + +import static org.alfresco.rest.actions.access.AccessRestrictionUtil.ERROR_MESSAGE_ACCESS_RESTRICTED; +import static org.alfresco.rest.rules.RulesTestsUtils.ADD_FEATURES_ACTION; +import static org.alfresco.rest.rules.RulesTestsUtils.COPY_ACTION; +import static org.alfresco.rest.rules.RulesTestsUtils.ID; +import static org.alfresco.rest.rules.RulesTestsUtils.INBOUND; +import static org.alfresco.rest.rules.RulesTestsUtils.INVERTED; +import static org.alfresco.rest.rules.RulesTestsUtils.IS_SHARED; +import static org.alfresco.rest.rules.RulesTestsUtils.RULE_ASYNC_DEFAULT; +import static org.alfresco.rest.rules.RulesTestsUtils.RULE_CASCADE_DEFAULT; +import static org.alfresco.rest.rules.RulesTestsUtils.RULE_ENABLED_DEFAULT; +import static org.alfresco.utility.constants.UserRole.SiteCollaborator; +import static org.alfresco.utility.report.log.Step.STEP; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.OK; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableMap; + +import org.alfresco.rest.RestTest; +import org.alfresco.rest.model.RestActionBodyExecTemplateModel; +import org.alfresco.rest.model.RestCompositeConditionDefinitionModel; +import org.alfresco.rest.model.RestRuleModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Tests for PUT /nodes/{nodeId}/rule-sets/{ruleSetId}/rules. + */ +@Test (groups = { TestGroup.RULES }) +public class UpdateRulesTests extends RestTest +{ + private UserModel user; + private SiteModel site; + private FolderModel ruleFolder; + + @BeforeClass (alwaysRun = true) + public void dataPreparation() + { + user = dataUser.createRandomTestUser(); + site = dataSite.usingUser(user).createPublicRandomSite(); + ruleFolder = dataContent.usingUser(user).usingSite(site).createFolder(); + } + + /** + * Check we can update a rule. + *

+ * Also check that the isShared field is not returned when not requested. + */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void updateRule() + { + RestRuleModel rule = createAndSaveRule("Rule name"); + + STEP("Try to update the rule."); + RestRuleModel updatedRuleModel = rulesUtils.createRuleModel("Updated rule name"); + RestRuleModel updatedRule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), updatedRuleModel); + + restClient.assertStatusCodeIs(OK); + updatedRule.assertThat().field(ID).is(rule.getId()) + .assertThat().field("name").is("Updated rule name") + .assertThat().field(IS_SHARED).isNull(); + } + + /** Check we get a 404 if trying to update a rule in a folder that doesn't exist. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void updateRuleForNonExistentFolder() + { + RestRuleModel rule = createAndSaveRule("Rule name"); + + STEP("Try to update a rule in a non-existent folder."); + FolderModel nonExistentFolder = FolderModel.getRandomFolderModel(); + nonExistentFolder.setNodeRef("fake-id"); + + RestRuleModel updatedRuleModel = new RestRuleModel(); + updatedRuleModel.setName("Updated rule name"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(nonExistentFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), updatedRuleModel); + + restClient.assertLastError().statusCodeIs(NOT_FOUND) + .containsSummary("Folder with id fake-id was not found"); + } + + /** Check we get a 404 if trying to update a rule in a rule set that doesn't exist. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void updateRuleForNonExistentRuleSet() + { + RestRuleModel rule = createAndSaveRule("Rule name"); + + STEP("Try to update a rule in a non-existent rule set."); + RestRuleModel updatedRuleModel = new RestRuleModel(); + updatedRuleModel.setName("Updated rule name"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingRuleSet("fake-id") + .updateRule(rule.getId(), updatedRuleModel); + + restClient.assertLastError().statusCodeIs(NOT_FOUND) + .containsSummary("Rule set with id fake-id was not found"); + } + + /** Check we get a 404 if trying to update a rule that doesn't exist. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void updateRuleForNonExistentRuleId() + { + STEP("Try to update a rule that doesn't exist."); + RestRuleModel updatedRuleModel = new RestRuleModel(); + updatedRuleModel.setName("Updated rule name"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule("fake-id", updatedRuleModel); + + restClient.assertLastError().statusCodeIs(NOT_FOUND) + .containsSummary("fake-id was not found"); + } + + /** Check that a user without permission cannot update a rule. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void requirePermissionToUpdateRule() + { + STEP("Create a user and use them to create a private site containing a folder"); + UserModel privateUser = dataUser.createRandomTestUser(); + SiteModel privateSite = dataSite.usingUser(privateUser).createPrivateRandomSite(); + FolderModel privateFolder = dataContent.usingUser(privateUser).usingSite(privateSite).createFolder(); + + STEP("Create a collaborator and check they don't have permission to create a rule"); + UserModel collaborator = dataUser.createRandomTestUser(); + dataUser.addUserToSite(collaborator, privateSite, SiteCollaborator); + RestRuleModel ruleModel = new RestRuleModel(); + ruleModel.setName("ruleName"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(privateFolder).usingDefaultRuleSet().createSingleRule(ruleModel); + + restClient.assertStatusCodeIs(FORBIDDEN); + restClient.assertLastError().containsSummary("Insufficient permissions to manage rules"); + } + + /** Check we get an error trying to update a rule to have no name. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void updateRuleToHaveEmptyName() + { + RestRuleModel rule = createAndSaveRule("Rule name"); + + STEP("Try to update the rule to have no name."); + RestRuleModel updatedRuleModel = rulesUtils.createRuleModel(""); + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet().updateRule(rule.getId(), updatedRuleModel); + + restClient.assertLastError().statusCodeIs(BAD_REQUEST) + .containsSummary("Rule name is a mandatory parameter"); + } + + /** Check that updates to the rule's id are ignored. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES }) + public void tryToUpdateRuleId() + { + RestRuleModel rule = createAndSaveRule("Rule name"); + + STEP("Try to update the rule id and check it isn't changed."); + RestRuleModel updatedRuleModel = rulesUtils.createRuleModel("Rule name"); + updatedRuleModel.setId("new-rule-id"); + RestRuleModel updatedRule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), updatedRuleModel); + + updatedRule.assertThat().field(ID).is(rule.getId()); + } + + /** Check we can update a rule and get the included fields. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void updateRuleWithIncludedFields() + { + RestRuleModel rule = createAndSaveRule("Rule name"); + + STEP("Try to update the rule."); + RestRuleModel updatedRuleModel = rulesUtils.createRuleModel("Updated rule name"); + RestRuleModel updatedRule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .include(IS_SHARED) + .updateRule(rule.getId(), updatedRuleModel); + + updatedRule.assertThat().field(IS_SHARED).isNotNull(); + } + + /** + * Check we get error when attempt to update a rule to one without any actions. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void updateRuleWithoutActionsShouldFail() + { + RestRuleModel rule = createAndSaveRule("Rule name"); + + STEP("Try to update the rule - set no actions."); + rule.setActions(null); + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .include(IS_SHARED) + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary("A rule must have at least one action"); + } + + /** + * Check we get error when attempt to update a rule to one with invalid action. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void updateRuleWithInvalidActionDefinitionShouldFail() + { + RestRuleModel rule = createAndSaveRule("Rule name"); + + STEP("Try to update the rule - set no actions."); + final RestActionBodyExecTemplateModel invalidAction = new RestActionBodyExecTemplateModel(); + final String actionDefinitionId = "invalid-definition-value"; + invalidAction.setActionDefinitionId(actionDefinitionId); + invalidAction.setParams(Map.of("dummy-key", "dummy-value")); + rule.setActions(List.of(invalidAction)); + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .include(IS_SHARED) + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary(String.format("Invalid rule action definition requested %s", actionDefinitionId)); + } + + /** Check we can use the POST response to create the new rule. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void updateCopyRuleWithResponseFromPOST() + { + FolderModel destination = dataContent.usingUser(user).usingSite(site).createFolder(); + + RestActionBodyExecTemplateModel copyAction = new RestActionBodyExecTemplateModel(); + copyAction.setActionDefinitionId(COPY_ACTION); + copyAction.setParams(ImmutableMap.of("destination-folder", destination.getNodeRef())); + RestRuleModel rule = createAndSaveRule("Rule name", List.of(copyAction)); + + STEP("Try to update the rule."); + rule.setName("Updated rule name"); + RestRuleModel updatedRule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .include(IS_SHARED) + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(OK); + updatedRule.assertThat().field("name").is("Updated rule name") + .assertThat().field("actions.actionDefinitionId").is(List.of(COPY_ACTION)) + .assertThat().field("actions.params").is(List.of(ImmutableMap.of("destination-folder", destination.getNodeRef()))); + } + + /** Check we can use the POST response and update rule fields. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void updateRuleFields() + { + final RestRuleModel rule = createAndSaveRule(rulesUtils.createRuleModelWithModifiedValues()); + + STEP("Try to update the rule fields."); + rule.setName("Updated rule name"); + rule.setTriggers(List.of(INBOUND)); + final String updatedDescription = "Updated description"; + rule.setDescription(updatedDescription); + rule.setIsEnabled(!RULE_ENABLED_DEFAULT); + rule.setIsInheritable(!RULE_CASCADE_DEFAULT); + rule.setIsAsynchronous(!RULE_ASYNC_DEFAULT); + rule.setErrorScript(null); + final RestRuleModel updatedRule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(OK); + updatedRule.assertThat().isEqualTo(rule, ID) + .assertThat().field(ID).isNotNull(); + } + + /** Check we can use the POST response and update rule by adding conditions. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void updateRuleAddConditions() + { + final RestRuleModel rule = createAndSaveRule(rulesUtils.createRuleModelWithModifiedValues()); + + STEP("Try to update the rule and add conditions."); + rule.setConditions(rulesUtils.createVariousConditions()); + + final RestRuleModel updatedRule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(OK); + updatedRule.assertThat().isEqualTo(rule, ID) + .assertThat().field(ID).isNotNull(); + } + + /** Check we can use the POST response and update a rule rule without any conditions by adding null conditions. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void updateRuleAddNullConditions() + { + final RestRuleModel rule = createAndSaveRule(rulesUtils.createRuleModelWithModifiedValues()); + + STEP("Try to update the rule and add null conditions."); + rule.setConditions(null); + + final RestRuleModel updatedRule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(OK); + updatedRule.assertThat().isEqualTo(rule, ID) + .assertThat().field(ID).isNotNull(); + } + + /** Check we can use the POST response and update rule by modifying conditions. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void updateRuleModifyConditions() + { + final RestRuleModel ruleModelWithInitialValues = rulesUtils.createRuleModelWithModifiedValues(); + ruleModelWithInitialValues.setConditions(rulesUtils.createVariousConditions()); + final RestRuleModel rule = createAndSaveRule(ruleModelWithInitialValues); + + STEP("Try to update the rule and modify conditions."); + final RestCompositeConditionDefinitionModel compositeCondition = rulesUtils.createCompositeCondition( + List.of(rulesUtils.createCompositeCondition(false, List.of(rulesUtils.createSimpleCondition("tag", "equals", "sample_tag"))))); + rule.setConditions(compositeCondition); + + final RestRuleModel updatedRule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(OK); + updatedRule.assertThat().isEqualTo(rule, ID) + .assertThat().field(ID).isNotNull(); + } + + /** Check we can use the POST response and update rule by removing all conditions. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void updateRuleRemoveAllConditions() + { + final RestRuleModel ruleModelWithInitialValues = rulesUtils.createRuleModelWithModifiedValues(); + ruleModelWithInitialValues.setConditions(rulesUtils.createVariousConditions()); + final RestRuleModel rule = createAndSaveRule(ruleModelWithInitialValues); + + STEP("Try to update the rule and remove all conditions."); + rule.setConditions(null); + + final RestRuleModel updatedRule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(OK); + updatedRule.assertThat().isEqualTo(rule, ID) + .assertThat().field(ID).isNotNull(); + } + + /** Check we get a 400 error when using the POST response and update rule by adding condition with invalid category. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void updateRuleWithInvalidCategoryInConditionAndFail() + { + final RestRuleModel ruleModelWithInitialValues = rulesUtils.createRuleModelWithModifiedValues(); + ruleModelWithInitialValues.setConditions(rulesUtils.createVariousConditions()); + final RestRuleModel rule = createAndSaveRule(ruleModelWithInitialValues); + + STEP("Try to update the rule with invalid condition."); + final RestCompositeConditionDefinitionModel conditions = rulesUtils.createCompositeCondition( + List.of(rulesUtils.createCompositeCondition(!INVERTED, List.of(rulesUtils.createSimpleCondition("category", "equals", "fake-category-id"))))); + rule.setConditions(conditions); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary("Category in condition is invalid"); + } + + /** Check we get a 400 error when using the POST response and update rule by adding condition without comparator when it is required. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void updateRuleWithConditionWithoutComparatorAndFail() + { + final RestRuleModel ruleModelWithInitialValues = rulesUtils.createRuleModelWithModifiedValues(); + ruleModelWithInitialValues.setConditions(rulesUtils.createVariousConditions()); + final RestRuleModel rule = createAndSaveRule(ruleModelWithInitialValues); + + STEP("Try to update the rule with invalid condition (null comparator when required non-null)."); + final RestCompositeConditionDefinitionModel conditions = rulesUtils.createCompositeCondition( + List.of(rulesUtils.createCompositeCondition(!INVERTED, List.of(rulesUtils.createSimpleCondition("size", null, "65500"))))); + rule.setConditions(conditions); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary("Comparator in condition must not be blank"); + } + + /** Check we get a 400 error when using the POST response and update rule by adding condition without field. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void updateRuleWithConditionWithoutFieldAndFail() + { + final RestRuleModel ruleModelWithInitialValues = rulesUtils.createRuleModelWithModifiedValues(); + ruleModelWithInitialValues.setConditions(rulesUtils.createVariousConditions()); + final RestRuleModel rule = createAndSaveRule(ruleModelWithInitialValues); + + STEP("Try to update the rule with invalid condition (null field)."); + final RestCompositeConditionDefinitionModel conditions = rulesUtils.createCompositeCondition( + List.of(rulesUtils.createCompositeCondition(!INVERTED, List.of(rulesUtils.createSimpleCondition(null, "greater_than", "65500"))))); + rule.setConditions(conditions); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary("Field in condition must not be blank"); + } + + /** Check we get a 400 error when using the POST response and update rule by adding condition without parameter value. */ + @Test (groups = { TestGroup.REST_API, TestGroup.RULES, TestGroup.SANITY }) + public void updateRuleWithConditionWithoutParamValueAndFail() + { + final RestRuleModel ruleModelWithInitialValues = rulesUtils.createRuleModelWithModifiedValues(); + ruleModelWithInitialValues.setConditions(rulesUtils.createVariousConditions()); + final RestRuleModel rule = createAndSaveRule(ruleModelWithInitialValues); + + STEP("Try to update the rule with invalid condition (null parameter)."); + final RestCompositeConditionDefinitionModel conditions = rulesUtils.createCompositeCondition( + List.of(rulesUtils.createCompositeCondition(!INVERTED, List.of(rulesUtils.createSimpleCondition("size", "greater_than", ""))))); + rule.setConditions(conditions); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary("Parameter in condition must not be blank"); + } + + /** + * Check we can update a rule by adding several actions. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void updateRuleAddActions() + { + final RestRuleModel rule = createAndSaveRule(rulesUtils.createRuleModelWithModifiedValues()); + + STEP("Try to update the rule by adding several actions"); + final RestActionBodyExecTemplateModel counterAction = rulesUtils.createCustomActionModel("counter", null); + final Map addAspectParams = Map.of("aspect-name", "cm:taggable"); + final RestActionBodyExecTemplateModel addAspectAction = rulesUtils.createCustomActionModel(ADD_FEATURES_ACTION, addAspectParams); + rule.setActions(Arrays.asList(counterAction, addAspectAction)); + + final RestRuleModel updatedRule = restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(OK); + updatedRule.assertThat().isEqualTo(rule, ID) + .assertThat().field(ID).isNotNull(); + } + + /** + * Check we get a 400 error when attempting to update a rule by adding action with not allowed parameter. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void updateRuleAddCheckoutActionForOutboundShouldFail() + { + final RestRuleModel rule = createAndSaveRule(rulesUtils.createRuleModelWithModifiedValues()); + + STEP("Try to update the rule by adding checkout action"); + final Map checkOutParams = + Map.of("destination-folder", rulesUtils.getCheckOutDestinationFolder().getNodeRef(), "assoc-name", "cm:checkout", + "assoc-type", "cm:contains"); + final RestActionBodyExecTemplateModel checkOutAction = rulesUtils.createCustomActionModel("check-out", checkOutParams); + rule.setActions(List.of(checkOutAction)); + + final UserModel admin = dataUser.getAdminUser(); + restClient.authenticateUser(admin).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary("Check out action cannot be performed for the rule type outbound!"); + } + + /** + * Check we get a 500 error when attempting to update a rule by adding action with parameter with non existing namespace in value. + * In near future we need to fix this kind of negative path to return a 4xx error. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.RULES}) + public void updateRuleAddActionWithInvalidParamShouldFail() + { + final RestRuleModel rule = createAndSaveRule(rulesUtils.createRuleModelWithModifiedValues()); + + STEP("Try to update the rule by adding action with invalid parameter (non-existing namespace in value)"); + final RestActionBodyExecTemplateModel action = new RestActionBodyExecTemplateModel(); + action.setActionDefinitionId(ADD_FEATURES_ACTION); + final String aspectNameParam = "aspect-name"; + final String paramValue = "dummy:dummy"; + action.setParams(Map.of(aspectNameParam, paramValue)); + rule.setActions(List.of(action)); + + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(BAD_REQUEST); + restClient.assertLastError().containsSummary( + String.format("Action parameter: %s has invalid value (%s). Look up possible values for constraint name %s", + aspectNameParam, paramValue, "ac-aspects")); + } + + /** Check that a normal user cannot create rules that use private actions. */ + @Test + public void updateRuleWithActions_userCannotUsePrivateAction() + { + STEP("Using admin create a rule with a private action."); + RestRuleModel rule = restClient.authenticateUser(dataUser.getAdminUser()).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(rulesUtils.createRuleWithPrivateAction()); + + STEP("Try to update the rule with a normal user."); + rule.setName("Updated name"); + restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(FORBIDDEN) + .assertLastError().containsSummary(ERROR_MESSAGE_ACCESS_RESTRICTED); + } + + /** Check that an administrator can create rules that use private actions. */ + @Test + public void updateRuleWithActions_adminCanUsePrivateAction() + { + STEP("Using admin create a rule with a private action."); + RestRuleModel rule = restClient.authenticateUser(dataUser.getAdminUser()).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(rulesUtils.createRuleWithPrivateAction()); + + STEP("Try to update the rule with the admin user."); + rule.setName("Updated name"); + RestRuleModel updatedRule = restClient.authenticateUser(dataUser.getAdminUser()).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .updateRule(rule.getId(), rule); + + restClient.assertStatusCodeIs(OK); + updatedRule.assertThat().field("name").is("Updated name"); + } + + private RestRuleModel createAndSaveRule(String name) + { + return createAndSaveRule(name, List.of(rulesUtils.createAddAudioAspectAction())); + } + + /** + * Create a rule for folder and store it. + * + * @param name The name for the rule. + * @param restActionModels Rule's actions. + * @return The created rule. + */ + private RestRuleModel createAndSaveRule(String name, List restActionModels) + { + STEP("Create a rule called " + name + ", containing actions: " + restActionModels); + RestRuleModel ruleModel = rulesUtils.createRuleModel(name, restActionModels); + return restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + } + + /** + * Create a rule for folder and store it. + * + * @param ruleModel RuleModel used as create request + * @return The created rule. + */ + private RestRuleModel createAndSaveRule(final RestRuleModel ruleModel) + { + STEP("Create a rule: " + ruleModel); + return restClient.authenticateUser(user).withPrivateAPI().usingNode(ruleFolder).usingDefaultRuleSet() + .createSingleRule(ruleModel); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/workflow/deployments/GetDeploymentsSanityTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/workflow/deployments/GetDeploymentsSanityTests.java index 9d1a4025f5..f08adacf13 100644 --- a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/workflow/deployments/GetDeploymentsSanityTests.java +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/workflow/deployments/GetDeploymentsSanityTests.java @@ -35,10 +35,11 @@ public class GetDeploymentsSanityTests extends RestTest restClient.assertStatusCodeIs(HttpStatus.OK); deployments.assertThat().entriesListIsNotEmpty(); deployments.getOneRandomEntry().onModel().assertThat() - .fieldsCount().is(3).and() + .fieldsCount().is(4).and() .field("id").isNotEmpty().and() .field("deployedAt").isNotEmpty().and() - .field("name").isNotEmpty(); + .field("name").isNotEmpty().and() + .field("category").isNotEmpty(); } } diff --git a/packaging/tests/tas-restapi/src/test/resources/test-suites/part1-suite.xml b/packaging/tests/tas-restapi/src/test/resources/test-suites/part1-suite.xml index ff2ffaed47..e74476b4c6 100644 --- a/packaging/tests/tas-restapi/src/test/resources/test-suites/part1-suite.xml +++ b/packaging/tests/tas-restapi/src/test/resources/test-suites/part1-suite.xml @@ -1,11 +1,11 @@ - + - + @@ -14,6 +14,7 @@ + diff --git a/packaging/tests/tas-webdav/pom.xml b/packaging/tests/tas-webdav/pom.xml index a160492e66..10d05eadf9 100644 --- a/packaging/tests/tas-webdav/pom.xml +++ b/packaging/tests/tas-webdav/pom.xml @@ -9,7 +9,7 @@ org.alfresco alfresco-community-repo-tests - 17.43-SNAPSHOT + 20.5-SNAPSHOT diff --git a/packaging/war/pom.xml b/packaging/war/pom.xml index 0bc57ad783..9d711afcbb 100644 --- a/packaging/war/pom.xml +++ b/packaging/war/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo-packaging - 17.43-SNAPSHOT + 20.5-SNAPSHOT diff --git a/packaging/war/src/main/resources/alfresco/web-client-security-config.xml b/packaging/war/src/main/resources/alfresco/web-client-security-config.xml index 0d5362bd14..9098134659 100644 --- a/packaging/war/src/main/resources/alfresco/web-client-security-config.xml +++ b/packaging/war/src/main/resources/alfresco/web-client-security-config.xml @@ -139,6 +139,33 @@ {token} + + + GET + /s/index|/s/ + + + {token} + {token} + + + + + POST + /s/index|/s/ + + + {token} + {token} + + + {referer} + + + {origin} + + + - 5.16.1 + 3.18.2 + 4.1.79.Final + 4.1.72.Final + 2.0.53.Final + 5.17.1 1.21 1.2.5 4.2.0 @@ -107,24 +109,23 @@ 2.7.0 1.1.4 - 3.2.1.3 - 1.4.1 - 7.2.0 + 3.3.1-A1 + 1.5.0 + 7.3.0 2.2.0 2.0.1.alfresco-2 - 42.4.0 - 8.0.29 + 42.5.0 + 8.0.30 8 2.7.4 - 3.0.48 - 3.3.0 - 1.87 - 1.31 - 1.8 - 1.6 - 1.5 + 3.0.56 + 5.2.0 + 1.135 + 1.9 + 1.7 + 1.7 2.6 @@ -405,7 +406,7 @@ org.apache.xmlbeans xmlbeans - 5.1.0 + 5.1.1 org.json @@ -501,7 +502,7 @@ com.fasterxml.jackson.core jackson-databind - ${dependency.jackson-databind.version} + ${dependency.jackson.version} com.fasterxml.jackson.datatype @@ -513,6 +514,11 @@ jackson-dataformat-yaml ${dependency.jackson.version} + + org.yaml + snakeyaml + 1.32 + com.fasterxml.jackson.core jackson-annotations @@ -616,6 +622,12 @@ org.slf4j slf4j-reload4j ${dependency.slf4j.version} + + + ch.qos.reload4j + reload4j + + xerces @@ -639,7 +651,7 @@ com.github.junrar junrar - 7.5.2 + 7.5.3 com.github.fge @@ -650,7 +662,7 @@ org.jsoup jsoup - 1.15.2 + 1.15.3 @@ -701,11 +713,6 @@ restapi ${dependency.tas-restapi.version} - - org.alfresco.tas - cmis - ${dependency.tas-cmis.version} - org.alfresco.tas email @@ -729,7 +736,7 @@ joda-time joda-time - 2.10.14 + 2.11.1 @@ -845,6 +852,18 @@ camel-mock ${dependency.camel.version} + + + io.netty + netty-handler-proxy + ${dependency.netty.version} + + + io.netty + netty-tcnative-classes + ${dependency.netty-tcnative.version} + + org.apache.taglibs taglibs-standard-spec @@ -877,6 +896,17 @@ 1.18.24 provided + + org.activiti + activiti-spring + ${dependency.activiti.version} + + + commons-dbcp + commons-dbcp + + + @@ -894,7 +924,7 @@ io.fabric8 docker-maven-plugin - 0.40.1 + 0.40.2 maven-surefire-plugin @@ -915,12 +945,12 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.4.0 + 3.4.1 org.apache.maven.plugins maven-resources-plugin - 3.2.0 + 3.3.0 org.apache.maven.plugins @@ -929,7 +959,7 @@ maven-assembly-plugin - 3.4.1 + 3.4.2 org.alfresco.maven.plugin diff --git a/remote-api/pom.xml b/remote-api/pom.xml index 3b4867c83c..8eda9b6d6e 100644 --- a/remote-api/pom.xml +++ b/remote-api/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo - 17.43-SNAPSHOT + 20.5-SNAPSHOT @@ -130,7 +130,7 @@ org.eclipse.jetty jetty-server - 8.2.0.v20160908 + 10.0.11 test @@ -142,19 +142,19 @@ org.eclipse.jetty jetty-security - 8.2.0.v20160908 + 10.0.11 test org.eclipse.jetty jetty-webapp - 8.2.0.v20160908 + 10.0.11 test org.alfresco.cmis.client alfresco-opencmis-extension - 2.0 + 2.1 test diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/MimeTypeUtil.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/MimeTypeUtil.java new file mode 100644 index 0000000000..9d245ad4ea --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/MimeTypeUtil.java @@ -0,0 +1,64 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.web.scripts; + +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.springframework.extensions.webscripts.WebScriptRequest; + +public class MimeTypeUtil +{ + + /** + * Get the file mimetype from the file ContentReader, and if its null then set the mimetype to binary by default + * and try to get the correct one from file extension + * + * + * @param reader reader of the file in the request + * @param req request relating to the file + * @param mimetypeService MimetypeService + * + * @return mimetype of the file as a string + */ + public static String determineMimetype(ContentReader reader, WebScriptRequest req, MimetypeService mimetypeService) + { + String mimetype = reader.getMimetype(); + if (mimetype == null || mimetype.length() == 0) + { + String extensionPath = req.getExtensionPath(); + mimetype = MimetypeMap.MIMETYPE_BINARY; + int extIndex = extensionPath.lastIndexOf('.'); + if (extIndex != -1) + { + String ext = extensionPath.substring(extIndex + 1); + mimetype = mimetypeService.getMimetype(ext); + } + } + return mimetype; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentGet.java index 83e32679d7..1efa6b42ee 100644 --- a/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentGet.java +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentGet.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -26,14 +26,18 @@ package org.alfresco.repo.web.scripts.content; import java.io.IOException; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletResponse; import org.alfresco.model.ContentModel; +import org.alfresco.repo.web.scripts.MimeTypeUtil; import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.NamespaceService; @@ -65,6 +69,19 @@ public class ContentGet extends StreamContent implements ServletContextAware private NamespaceService namespaceService; private ContentService contentService; + private List nonAttachContentTypes = Collections.emptyList(); + + /** + * @param nonAttachContentTypes List + */ + public void setNonAttachContentTypes(List nonAttachContentTypes) + { + if (nonAttachContentTypes != null && !nonAttachContentTypes.isEmpty()) + { + this.nonAttachContentTypes = nonAttachContentTypes; + } + } + /** * @param servletContext ServletContext */ @@ -121,9 +138,7 @@ public class ContentGet extends StreamContent implements ServletContextAware { throw new WebScriptException(HttpServletResponse.SC_NOT_FOUND, "Unable to find " + reference.toString()); } - - // determine attachment - boolean attach = Boolean.valueOf(req.getParameter("a")); + // render content QName propertyQName = ContentModel.PROP_CONTENT; @@ -140,6 +155,19 @@ public class ContentGet extends StreamContent implements ServletContextAware propertyQName = QName.createQName(propertyName, namespaceService); } } + // determine attachment and force download for specific mimetypes - see PRODSEC-5862 + boolean attach = Boolean.valueOf(req.getParameter("a")); + ContentReader reader = contentService.getReader(nodeRef, propertyQName); + String mimetype = MimeTypeUtil.determineMimetype(reader, req, mimetypeService); + + if (!attach) + { + if (nonAttachContentTypes == null || !nonAttachContentTypes.contains(mimetype)) + { + attach = true; + logger.warn("Ignored a=false for " + nodeRef.getId() + " since " + mimetype + " is not in the whitelist for non-attach content types"); + } + } // Stream the content streamContentLocal(req, res, nodeRef, attach, propertyQName, null); diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentInfo.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentInfo.java index 2e34694c24..9e8b8c2572 100644 --- a/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentInfo.java +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentInfo.java @@ -1,28 +1,28 @@ -/* - * #%L - * Alfresco Remote API - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.repo.web.scripts.content; import java.io.IOException; @@ -33,7 +33,7 @@ import java.util.Map; import javax.servlet.http.HttpServletResponse; import org.alfresco.model.ContentModel; -import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.repo.web.scripts.MimeTypeUtil; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.QName; @@ -79,18 +79,7 @@ public class ContentInfo extends StreamContent delegate.setAttachment(req, res, attach, attachFileName); // establish mimetype - String mimetype = reader.getMimetype(); - String extensionPath = req.getExtensionPath(); - if (mimetype == null || mimetype.length() == 0) - { - mimetype = MimetypeMap.MIMETYPE_BINARY; - int extIndex = extensionPath.lastIndexOf('.'); - if (extIndex != -1) - { - String ext = extensionPath.substring(extIndex + 1); - mimetype = mimetypeService.getMimetype(ext); - } - } + String mimetype = MimeTypeUtil.determineMimetype(reader, req, mimetypeService); // set mimetype for the content and the character encoding + length for the stream res.setContentType(mimetype); diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentStreamer.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentStreamer.java index 75a4948cd5..846ecb9401 100644 --- a/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentStreamer.java +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/ContentStreamer.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -30,6 +30,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.UnsupportedEncodingException; import java.net.SocketException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -43,6 +44,7 @@ import javax.servlet.http.HttpServletResponse; import org.alfresco.model.ContentModel; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.content.filestore.FileContentReader; +import org.alfresco.repo.web.scripts.MimeTypeUtil; import org.alfresco.sync.repo.events.EventPublisher; import org.alfresco.repo.web.util.HttpRangeProcessor; import org.alfresco.rest.framework.resource.content.CacheDirective; @@ -58,6 +60,7 @@ import org.alfresco.service.namespace.QName; import org.alfresco.util.TempFileProvider; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.cxf.attachment.Rfc5987Util; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.ResourceLoader; import org.springframework.extensions.webscripts.Cache; @@ -361,18 +364,7 @@ public class ContentStreamer implements ResourceLoaderAware setAttachment(req, res, attach, attachFileName); // establish mimetype - String mimetype = reader.getMimetype(); - String extensionPath = req.getExtensionPath(); - if (mimetype == null || mimetype.length() == 0) - { - mimetype = MimetypeMap.MIMETYPE_BINARY; - int extIndex = extensionPath.lastIndexOf('.'); - if (extIndex != -1) - { - String ext = extensionPath.substring(extIndex + 1); - mimetype = mimetypeService.getMimetype(ext); - } - } + String mimetype = MimeTypeUtil.determineMimetype(reader, req, mimetypeService); res.setHeader(HEADER_ACCEPT_RANGES, "bytes"); try @@ -482,7 +474,7 @@ public class ContentStreamer implements ResourceLoaderAware if (req == null) { - headerValue += "; filename*=UTF-8''" + URLEncoder.encode(attachFileName, StandardCharsets.UTF_8) + headerValue += "; filename*=UTF-8''" + encodeFilename(attachFileName) + "; filename=\"" + filterNameForQuotedString(attachFileName) + "\""; } else @@ -491,12 +483,12 @@ public class ContentStreamer implements ResourceLoaderAware boolean isLegacy = (null != userAgent) && (userAgent.contains("MSIE 8") || userAgent.contains("MSIE 7")); if (isLegacy) { - headerValue += "; filename=\"" + URLEncoder.encode(attachFileName, StandardCharsets.UTF_8); + headerValue += "; filename=\"" + encodeFilename(attachFileName); } else { headerValue += "; filename=\"" + filterNameForQuotedString(attachFileName) + "\"; filename*=UTF-8''" - + URLEncoder.encode(attachFileName, StandardCharsets.UTF_8); + + encodeFilename(attachFileName); } } } @@ -506,6 +498,21 @@ public class ContentStreamer implements ResourceLoaderAware res.setHeader("Content-Disposition", headerValue); } } + + private String encodeFilename(String attachFileName) + { + try + { + return Rfc5987Util.encode(attachFileName); + } + catch (UnsupportedEncodingException e) + { + if (logger.isInfoEnabled()) + logger.info(e.getMessage() + " Changing encoder from Rfc5987Util to java.net.URLEncoder."); + + return URLEncoder.encode(attachFileName, StandardCharsets.UTF_8); + } + } protected String filterNameForQuotedString(String s) { diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/MimetypesGet.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/MimetypesGet.java index d9a0b0a278..10a766e701 100644 --- a/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/MimetypesGet.java +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/content/MimetypesGet.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -36,7 +36,7 @@ import org.alfresco.repo.content.metadata.MetadataExtracter; import org.alfresco.repo.content.metadata.MetadataExtracterRegistry; import org.alfresco.repo.content.transform.LocalTransformServiceRegistry; import org.alfresco.service.cmr.repository.MimetypeService; -import org.alfresco.transform.client.registry.TransformServiceRegistry; +import org.alfresco.transform.registry.TransformServiceRegistry; import org.springframework.extensions.webscripts.Cache; import org.springframework.extensions.webscripts.DeclarativeWebScript; import org.springframework.extensions.webscripts.Status; diff --git a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/AbstractRuleWebScript.java b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/AbstractRuleWebScript.java index 9ad634fcfb..57919bce45 100644 --- a/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/AbstractRuleWebScript.java +++ b/remote-api/src/main/java/org/alfresco/repo/web/scripts/rule/AbstractRuleWebScript.java @@ -78,7 +78,7 @@ public abstract class AbstractRuleWebScript extends DeclarativeWebScript private static final String RULE_OUTBOUND = "outbound"; private static final String ACTION_CHECK_OUT = "check-out"; - private static final String CANNOT_CREATE_RULE = "cannot.create.rule.checkout.outbound"; + public static final String CANNOT_CREATE_RULE = "cannot.create.rule.checkout.outbound"; protected NodeService nodeService; protected RuleService ruleService; diff --git a/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVServlet.java b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVServlet.java index 2fcfb408c2..6c83b50659 100644 --- a/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVServlet.java +++ b/remote-api/src/main/java/org/alfresco/repo/webdav/WebDAVServlet.java @@ -121,6 +121,13 @@ public class WebDAVServlet extends HttpServlet startTime = System.currentTimeMillis(); } + if (request.getMethod().equals(WebDAV.METHOD_POST) && !initParams.allowInsecurePOSTMethod()) + { + logger.error("POST method is not allowed!"); + response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + return; + } + FileFilterMode.setClient(Client.webdav); try @@ -407,6 +414,7 @@ public class WebDAVServlet extends HttpServlet private String storeName; private String rootPath; private String urlPathPrefix; + private boolean allowInsecurePOSTMethod = false; public boolean getEnabled() { @@ -482,5 +490,15 @@ public class WebDAVServlet extends HttpServlet { this.urlPathPrefix = urlPathPrefix; } + + public boolean allowInsecurePOSTMethod() + { + return allowInsecurePOSTMethod; + } + + public void setAllowInsecurePOSTMethod(boolean allowInsecurePOSTMethod) + { + this.allowInsecurePOSTMethod = allowInsecurePOSTMethod; + } } } diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Actions.java b/remote-api/src/main/java/org/alfresco/rest/api/Actions.java index e45fa3ce60..0ba356e526 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/Actions.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/Actions.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2018 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -27,10 +27,14 @@ package org.alfresco.rest.api; +import java.util.List; + import org.alfresco.rest.api.model.Action; import org.alfresco.rest.api.model.ActionDefinition; +import org.alfresco.rest.api.model.ActionParameterConstraint; import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.Experimental; import org.alfresco.service.cmr.repository.NodeRef; public interface Actions @@ -45,7 +49,12 @@ public interface Actions { NAME, TITLE - }; + } Action executeAction(Action action, Parameters parameters); + + @Experimental + ActionParameterConstraint getActionConstraint(String constraintName); + @Experimental + ActionDefinition getRuleActionDefinitionById(String actionDefinitionId); } diff --git a/remote-api/src/main/java/org/alfresco/rest/api/RuleSets.java b/remote-api/src/main/java/org/alfresco/rest/api/RuleSets.java new file mode 100644 index 0000000000..a5bcfa2455 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/RuleSets.java @@ -0,0 +1,81 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api; + +import java.util.List; + +import org.alfresco.rest.api.model.rules.RuleSet; +import org.alfresco.rest.api.model.rules.RuleSetLink; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.service.Experimental; + +/** + * Rule sets API. + */ +@Experimental +public interface RuleSets +{ + /** + * Get rule sets for a folder. + * + * @param folderNodeId Folder node ID + * @param paging {@link Paging} information + * @param includes List of fields to include in the rule set + * @return {@link CollectionWithPagingInfo} containing a list page of rule sets + */ + CollectionWithPagingInfo getRuleSets(String folderNodeId, List includes, Paging paging); + + /** + * Get the rule set with the given ID and check associations with the folder node. + * + * @param folderNodeId Folder node ID + * @param ruleSetId Rule set ID + * @param includes List of fields to include in the rule set + * @return {@link RuleSet} definition + */ + RuleSet getRuleSetById(String folderNodeId, String ruleSetId, List includes); + + /** + * Update a rule set - for example to reorder the rules within it. + * + * @param folderNodeId Folder node ID + * @param ruleSet The updated rule set. + * @param includes List of fields to include in the response. + * @return The updated rule set from the server. + */ + RuleSet updateRuleSet(String folderNodeId, RuleSet ruleSet, List includes); + + /** + * Link a rule set to a folder + */ + RuleSetLink linkToRuleSet(String folderNodeId, String linkToNodeId); + + /** + * Removes the link between a rule set and a folder + */ + void unlinkRuleSet(String folderNodeId, String ruleSetId); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/RuleSettings.java b/remote-api/src/main/java/org/alfresco/rest/api/RuleSettings.java new file mode 100644 index 0000000000..13e8b5a07e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/RuleSettings.java @@ -0,0 +1,54 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api; + +import org.alfresco.rest.api.model.rules.RuleSetting; +import org.alfresco.service.Experimental; + +/** + * Rule settings API. + */ +@Experimental +public interface RuleSettings +{ + /** + * Get the rule setting with the given key. + * + * @param folderId Folder node ID + * @param ruleSettingKey Rule setting key + * @return {@link RuleSetting} The retrieved rule setting object. + */ + RuleSetting getRuleSetting(String folderId, String ruleSettingKey); + + /** + * Set the rule setting against the specified folder. + * + * @param folderId The folder to update. + * @param ruleSetting The new rule setting. + * @return The updated rule setting object. + */ + RuleSetting setRuleSetting(String folderId, RuleSetting ruleSetting); +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Rules.java b/remote-api/src/main/java/org/alfresco/rest/api/Rules.java index 2580936c55..9d7d222d66 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/Rules.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/Rules.java @@ -26,10 +26,15 @@ package org.alfresco.rest.api; +import java.util.List; + import org.alfresco.rest.api.model.rules.Rule; +import org.alfresco.rest.api.model.rules.RuleExecution; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; import org.alfresco.rest.framework.resource.parameters.Paging; import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.rule.RuleServiceException; /** * Folder node rules API. @@ -43,10 +48,11 @@ public interface Rules * * @param folderNodeId - folder node ID * @param ruleSetId - rule set ID + * @param includes - The list of optional fields to include in the response. * @param paging - {@link Paging} information * @return {@link CollectionWithPagingInfo} containing a list page of folder rules */ - CollectionWithPagingInfo getRules(String folderNodeId, String ruleSetId, Paging paging); + CollectionWithPagingInfo getRules(String folderNodeId, String ruleSetId, List includes, Paging paging); /** * Get rule for rule's ID and check associations with folder node and rule set node @@ -54,7 +60,50 @@ public interface Rules * @param folderNodeId - folder node ID * @param ruleSetId - rule set ID * @param ruleId - rule ID + * @param includes - The list of optional fields to include in the response. * @return {@link Rule} definition */ - Rule getRuleById(String folderNodeId, String ruleSetId, String ruleId); + Rule getRuleById(String folderNodeId, String ruleSetId, String ruleId, List includes); + + /** + * Create new rules (and potentially a rule set if "-default-" is supplied). + * + * @param folderNodeId The node id of a folder. + * @param ruleSetId The id of a rule set (or "-default-" to use/create the default rule set for the folder). + * @param rule The definition of the rule. + * @param includes The list of optional fields to include in the response. + * @return The newly created rules. + * @throws InvalidArgumentException If the nodes are not the expected types, or the rule set does not correspond to the folder. + * @throws RuleServiceException If the folder is already linked to another rule set. + */ + List createRules(String folderNodeId, String ruleSetId, List rule, List includes); + + /** + * Update a rule. + * + * @param folderNodeId The id of a folder. + * @param ruleSetId The id of a rule set within the folder (or "-default-" to use the default rule set for the folder). + * @param ruleId The rule id. + * @param rule The new version of the rule. + * @param includes The list of optional fields to include in the response. + * @return The newly updated rule. + */ + Rule updateRuleById(String folderNodeId, String ruleSetId, String ruleId, Rule rule, List includes); + + /** + * Delete rule for rule's ID and check associations with folder node and rule set node + * + * @param folderNodeId - folder node ID + * @param ruleSetId - rule set ID + * @param ruleId - rule ID + */ + void deleteRuleById(String folderNodeId, String ruleSetId, String ruleId); + + /** + * Execute rules for given folder node. + * + * @param folderNodeId - the ID of a folder + * @param eachSubFolderIncluded - indicates if rules should be executed also on sub-folders + */ + RuleExecution executeRules(final String folderNodeId, final boolean eachSubFolderIncluded); } diff --git a/remote-api/src/main/java/org/alfresco/rest/api/actions/ActionConstraintsEntityResource.java b/remote-api/src/main/java/org/alfresco/rest/api/actions/ActionConstraintsEntityResource.java new file mode 100644 index 0000000000..fa749d3310 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/actions/ActionConstraintsEntityResource.java @@ -0,0 +1,59 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.actions; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.rest.api.Actions; +import org.alfresco.rest.api.model.ActionParameterConstraint; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.Experimental; + +@EntityResource(name="action-constraints", title = "Action parameter constraints") +@Experimental +public class ActionConstraintsEntityResource implements EntityResourceAction.ReadById +{ + private final Actions actions; + + public ActionConstraintsEntityResource(Actions actions) + { + this.actions = actions; + } + + @WebApiDescription(title = "Get single action parameters constraint", + description = "Retrieves a single action parameters constraint by constraint name", + successStatus = HttpServletResponse.SC_OK) + @Experimental + @Override + public ActionParameterConstraint readById(String constraintName, Parameters parameters) throws EntityNotFoundException + { + return actions.getActionConstraint(constraintName); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/actions/ActionParameterConstraintsEntityResource.java b/remote-api/src/main/java/org/alfresco/rest/api/actions/ActionParameterConstraintsEntityResource.java new file mode 100644 index 0000000000..117d214e3c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/actions/ActionParameterConstraintsEntityResource.java @@ -0,0 +1,59 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.actions; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.rest.api.Actions; +import org.alfresco.rest.api.model.ActionParameterConstraint; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.Experimental; + +@EntityResource(name="action-parameter-constraints", title = "Action parameter constraints") +@Experimental +public class ActionParameterConstraintsEntityResource implements EntityResourceAction.ReadById +{ + private final Actions actions; + + public ActionParameterConstraintsEntityResource(Actions actions) + { + this.actions = actions; + } + + @WebApiDescription(title = "Get single action parameter constraint", + description = "Retrieves a single action parameter constraint by constraint name", + successStatus = HttpServletResponse.SC_OK) + @Experimental + @Override + public ActionParameterConstraint readById(String constraintName, Parameters parameters) throws EntityNotFoundException + { + return actions.getActionConstraint(constraintName); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/actions/ActionValidator.java b/remote-api/src/main/java/org/alfresco/rest/api/actions/ActionValidator.java new file mode 100644 index 0000000000..2b1af819a0 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/actions/ActionValidator.java @@ -0,0 +1,60 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.actions; + +import java.util.List; + +import org.alfresco.rest.api.model.rules.Action; +import org.alfresco.service.Experimental; + +@Experimental +public interface ActionValidator +{ + + String ALL_ACTIONS = "all"; + + /** + * Provides validation logic for given action. + */ + void validate(Action action); + + /** + * Returns priority of validator (applied to bulk validation in @see {@link org.alfresco.rest.api.impl.mapper.rules.RestRuleActionModelMapper}) + * The lower number, the higher priority is set for the validator. + * @return priority expressed as int + */ + int getPriority(); + + /** + * By default validator is applied to all actions + * + * @return indicator for all defined action definition ids + */ + default List getActionDefinitionIds() { + return List.of(ALL_ACTIONS); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/ActionsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/ActionsImpl.java index 2bc272ba0b..432a9ffa2c 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/impl/ActionsImpl.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/ActionsImpl.java @@ -27,15 +27,22 @@ package org.alfresco.rest.api.impl; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.action.access.ActionAccessRestriction; +import org.alfresco.repo.action.constraint.FolderContentsParameterConstraint; import org.alfresco.rest.api.Actions; +import org.alfresco.rest.api.impl.rules.ActionParameterConverter; import org.alfresco.rest.api.model.Action; import org.alfresco.rest.api.model.ActionDefinition; +import org.alfresco.rest.api.model.ActionParameterConstraint; import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.ListPage; import org.alfresco.rest.framework.resource.parameters.Parameters; import org.alfresco.rest.framework.resource.parameters.SortColumn; +import org.alfresco.service.Experimental; import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.action.ParameterConstraint; import org.alfresco.service.cmr.action.ParameterDefinition; import org.alfresco.service.cmr.dictionary.DataTypeDefinition; import org.alfresco.service.cmr.dictionary.DictionaryException; @@ -59,6 +66,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -68,11 +76,14 @@ import static java.util.Comparator.nullsFirst; public class ActionsImpl implements Actions { + static final String CONSTRAINT_NOT_EXISTS = "Action parameter constraints for name %s do not exist."; + private ActionService actionService; private DictionaryService dictionaryService; private NamespaceService namespaceService; private NodeService nodeService; private NamespacePrefixResolver prefixResolver; + private ActionParameterConverter actionParameterConverter; public void setActionService(ActionService actionService) { @@ -99,6 +110,11 @@ public class ActionsImpl implements Actions this.prefixResolver = prefixResolver; } + public void setActionParameterConverter(ActionParameterConverter actionParameterConverter) + { + this.actionParameterConverter = actionParameterConverter; + } + @Override public ActionDefinition getActionDefinitionById(String actionDefinitionId) { @@ -125,27 +141,13 @@ public class ActionsImpl implements Actions return result; } - - private ActionDefinition getActionDefinition( + + private ActionDefinition getActionDefinition( org.alfresco.service.cmr.action.ActionDefinition actionDefinitionId) - { - List paramDefs = - actionDefinitionId. - getParameterDefinitions(). - stream(). - map(this::toModel). - collect(Collectors.toList()); - return new ActionDefinition( - actionDefinitionId.getName(), // ID is a synonym for name. - actionDefinitionId.getName(), - actionDefinitionId.getTitle(), - actionDefinitionId.getDescription(), - toShortQNames(actionDefinitionId.getApplicableTypes()), - actionDefinitionId.getAdhocPropertiesAllowed(), - actionDefinitionId.getTrackStatus(), - paramDefs); + { + return mapFromServiceModel(actionDefinitionId); } - + @Override public CollectionWithPagingInfo getActionDefinitions(NodeRef nodeRef, Parameters params) { @@ -174,7 +176,7 @@ public class ActionsImpl implements Actions sortKey = Actions.SortKey.valueOf(sorting.get(0).column.toUpperCase()); sortAsc = sorting.get(0).asc; } - + Comparator comparator; switch (sortKey) { @@ -187,7 +189,7 @@ public class ActionsImpl implements Actions default: throw new IllegalArgumentException("Invalid sort key, must be either 'title' or 'name'."); } - + if (!sortAsc) { comparator = comparator.reversed(); @@ -199,28 +201,12 @@ public class ActionsImpl implements Actions List sortedPage = actionDefinitions. stream(). - map(actionDefinition -> { - List paramDefs = - actionDefinition. - getParameterDefinitions(). - stream(). - map(this::toModel). - collect(Collectors.toList()); - return new ActionDefinition( - actionDefinition.getName(), // ID is a synonym for name. - actionDefinition.getName(), - actionDefinition.getTitle(), - actionDefinition.getDescription(), - toShortQNames(actionDefinition.getApplicableTypes()), - actionDefinition.getAdhocPropertiesAllowed(), - actionDefinition.getTrackStatus(), - paramDefs); - }). + map(this::mapFromServiceModel). sorted(comparator). skip(skip). limit(maxItems). collect(Collectors.toList()); - + boolean hasMoreItems = actionDefinitions.size() > (skip + maxItems); return CollectionWithPagingInfo.asPaged( @@ -230,6 +216,40 @@ public class ActionsImpl implements Actions actionDefinitions.size()); } + @Override + @Experimental + public ActionDefinition getRuleActionDefinitionById(String actionDefinitionId) + { + if (actionDefinitionId == null) + { + throw new InvalidArgumentException("actionDefinitionId is null"); + } + return actionService.getActionDefinitions().stream() + .filter(a -> actionDefinitionId.equals(a.getName())) + .map(this::mapFromServiceModel) + .findFirst() + .orElseThrow(() -> new NotFoundException(NotFoundException.DEFAULT_MESSAGE_ID, new String[] {actionDefinitionId})); + } + + private ActionDefinition mapFromServiceModel(org.alfresco.service.cmr.action.ActionDefinition actionDefinition) + { + List paramDefs = + actionDefinition. + getParameterDefinitions(). + stream(). + map(this::toModel). + collect(Collectors.toList()); + return new ActionDefinition( + actionDefinition.getName(), // ID is a synonym for name. + actionDefinition.getName(), + actionDefinition.getTitle(), + actionDefinition.getDescription(), + toShortQNames(actionDefinition.getApplicableTypes()), + actionDefinition.getAdhocPropertiesAllowed(), + actionDefinition.getTrackStatus(), + paramDefs); + } + @Override public Action executeAction(Action action, Parameters parameters) { @@ -297,6 +317,52 @@ public class ActionsImpl implements Actions return result; } + @Override + @Experimental + public ActionParameterConstraint getActionConstraint(String constraintName) + { + final ParameterConstraint parameterConstraint = actionService.getParameterConstraint(constraintName); + if (Objects.isNull(parameterConstraint)) + { + throw new NotFoundException(String.format(CONSTRAINT_NOT_EXISTS, constraintName)); + } + return mapToActionConstraint(parameterConstraint); + } + + @Experimental + private ActionParameterConstraint mapToActionConstraint(ParameterConstraint parameterConstraint) + { + final ActionParameterConstraint constraint = new ActionParameterConstraint(); + constraint.setConstraintName(parameterConstraint.getName()); + constraint.setConstraintValues(getConstraintDataList(parameterConstraint)); + return constraint; + } + + @Experimental + private List getConstraintDataList(final ParameterConstraint parameterConstraint) + { + final Map constraintValues = parameterConstraint.getValues(); + if (parameterConstraint instanceof FolderContentsParameterConstraint) + { + return convertNodeRefConstraintValues(constraintValues).entrySet().stream() + .map(e -> new ActionParameterConstraint.ConstraintData(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + } else + { + return constraintValues.entrySet().stream() + .map(e -> new ActionParameterConstraint.ConstraintData(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + } + } + + @Experimental + private Map convertNodeRefConstraintValues(final Map inputValues) + { + return inputValues.entrySet().stream() + .collect(Collectors.toMap(e -> actionParameterConverter.convertParamFromServiceModel(new NodeRef(e.getKey())).toString(), + Map.Entry::getValue)); + } + private Map extractActionParams(org.alfresco.service.cmr.action.ActionDefinition actionDefinition, Map params) { Map parameterValues = new HashMap<>(); diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/CustomModelsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/CustomModelsImpl.java index 78f24ace73..5231eac589 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/impl/CustomModelsImpl.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/CustomModelsImpl.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2021 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -713,6 +713,7 @@ public class CustomModelsImpl implements CustomModels try { NodeRef nodeRef = customModelService.createDownloadNode(modelName, withForm); + nodeService.setProperty(nodeRef, ContentModel.PROP_NAME, modelName + DownloadsImpl.DEFAULT_ARCHIVE_EXTENSION); return new CustomModelDownload(nodeRef); } catch (Exception ex) diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/NodesImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/NodesImpl.java index fa3d09a7d1..435f5489d7 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/impl/NodesImpl.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/NodesImpl.java @@ -400,12 +400,10 @@ public class NodesImpl implements Nodes @Override public NodeRef validateNode(StoreRef storeRef, String nodeId) { - String versionLabel = null; - int idx = nodeId.indexOf(";"); if (idx != -1) { - versionLabel = nodeId.substring(idx + 1); + String versionLabel = nodeId.substring(idx + 1); nodeId = nodeId.substring(0, idx); if (versionLabel.equals("pwc")) { @@ -1753,7 +1751,7 @@ public class NodesImpl implements Nodes // default false (if not provided) boolean permanentDelete = Boolean.valueOf(parameters.getParameter(PARAM_PERMANENT)); - if (permanentDelete == true) + if (permanentDelete) { boolean isAdmin = authorityService.hasAdminAuthority(); if (! isAdmin) diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/RulesImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/RulesImpl.java deleted file mode 100644 index 5f0f38d069..0000000000 --- a/remote-api/src/main/java/org/alfresco/rest/api/impl/RulesImpl.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * #%L - * Alfresco Remote API - * %% - * Copyright (C) 2005 - 2022 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ - -package org.alfresco.rest.api.impl; - -import org.alfresco.model.ContentModel; -import org.alfresco.repo.rule.RuleModel; -import org.alfresco.rest.api.Nodes; -import org.alfresco.rest.api.Rules; -import org.alfresco.rest.api.model.rules.Rule; -import org.alfresco.rest.api.model.rules.RuleSet; -import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; -import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; -import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; -import org.alfresco.rest.framework.resource.parameters.ListPage; -import org.alfresco.rest.framework.resource.parameters.Paging; -import org.alfresco.service.Experimental; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.rule.RuleService; -import org.alfresco.service.cmr.security.AccessStatus; -import org.alfresco.service.cmr.security.PermissionService; -import org.alfresco.service.namespace.QName; - -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -@Experimental -public class RulesImpl implements Rules -{ - private static final String RULE_SET_EXPECTED_TYPE_NAME = "rule set"; - - private Nodes nodes; - - private PermissionService permissionService; - - private RuleService ruleService; - - @Override - public CollectionWithPagingInfo getRules(final String folderNodeId, final String ruleSetId, final Paging paging) - { - final NodeRef folderNodeRef = validateFolderNode(folderNodeId); - validateRuleSetNode(ruleSetId, folderNodeRef); - - final List rules = ruleService.getRules(folderNodeRef).stream() - .map(Rule::from) - .collect(Collectors.toList()); - - return ListPage.of(rules, paging); - } - - @Override - public Rule getRuleById(final String folderNodeId, final String ruleSetId, final String ruleId) - { - final NodeRef folderNodeRef = validateFolderNode(folderNodeId); - final NodeRef ruleSetNodeRef = validateRuleSetNode(ruleSetId, folderNodeRef); - final NodeRef ruleNodeRef = validateRuleNode(ruleId, ruleSetNodeRef); - - return Rule.from(ruleService.getRule(ruleNodeRef)); - } - - public void setNodes(Nodes nodes) - { - this.nodes = nodes; - } - - public void setPermissionService(PermissionService permissionService) - { - this.permissionService = permissionService; - } - - public void setRuleService(RuleService ruleService) - { - this.ruleService = ruleService; - } - - /** - * Validates if folder node exists and user have permission to read from it. - * - * @param folderNodeId - folder node ID - * @return folder node reference - * @throws InvalidArgumentException if node is not of an expected type - * @throws PermissionDeniedException if user doesn't have right to read from folder - */ - private NodeRef validateFolderNode(final String folderNodeId) - { - final NodeRef nodeRef = nodes.validateOrLookupNode(folderNodeId, null); - if (permissionService.hasReadPermission(nodeRef) != AccessStatus.ALLOWED) { - throw new PermissionDeniedException("Cannot read from this node!"); - } - - verifyNodeType(nodeRef, ContentModel.TYPE_FOLDER, null); - - return nodeRef; - } - - /** - * Validates if rule set ID is default, node exists and associated folder node matches. - * - * @param ruleSetId - rule set node ID - * @param associatedFolderNodeRef - folder node ref to check the association - * @return rule set node reference - * @throws InvalidArgumentException in case of not matching associated folder node - */ - private NodeRef validateRuleSetNode(final String ruleSetId, final NodeRef associatedFolderNodeRef) - { - if (RuleSet.isDefaultId(ruleSetId)) - { - return ruleService.getRuleSetNode(associatedFolderNodeRef); - } - - final NodeRef ruleSetNodeRef = validateNode(ruleSetId, ContentModel.TYPE_SYSTEM_FOLDER, RULE_SET_EXPECTED_TYPE_NAME); - if (!ruleService.isRuleSetAssociatedWithFolder(ruleSetNodeRef, associatedFolderNodeRef)) { - throw new InvalidArgumentException("Rule set is not associated with folder node!"); - } - - return ruleSetNodeRef; - } - - /** - * Validates if rule node exists and associated rule set node matches. - * - * @param ruleId - rule node ID - * @param associatedRuleSetNodeRef - rule set node ref to check the association. Can be null - * @return rule node reference - * @throws InvalidArgumentException in case of not matching associated rule set node - */ - private NodeRef validateRuleNode(final String ruleId, final NodeRef associatedRuleSetNodeRef) - { - final NodeRef ruleNodeRef = validateNode(ruleId, RuleModel.TYPE_RULE, null); - if (associatedRuleSetNodeRef != null && !ruleService.isRuleAssociatedWithRuleSet(ruleNodeRef, associatedRuleSetNodeRef)) - { - throw new InvalidArgumentException("Rule is not associated with rule set node!"); - } - - return ruleNodeRef; - } - - private NodeRef validateNode(final String nodeId, final QName expectedType, final String expectedTypeName) - { - final NodeRef nodeRef = nodes.validateNode(nodeId); - verifyNodeType(nodeRef, expectedType, expectedTypeName); - - return nodeRef; - } - - private void verifyNodeType(final NodeRef nodeRef, final QName expectedType, final String expectedTypeName) { - final Set expectedTypes = Set.of(expectedType); - if (!nodes.nodeMatches(nodeRef, expectedTypes, null)) { - final String expectedTypeLocalName = (expectedTypeName != null)? expectedTypeName : expectedType.getLocalName(); - throw new InvalidArgumentException(String.format("NodeId of a %s is expected!", expectedTypeLocalName)); - } - } -} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleActionModelMapper.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleActionModelMapper.java new file mode 100644 index 0000000000..35b8b3934e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleActionModelMapper.java @@ -0,0 +1,126 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.mapper.rules; + +import static java.util.Collections.emptyMap; + +import static org.alfresco.repo.action.access.ActionAccessRestriction.ACTION_CONTEXT_PARAM_NAME; +import static org.alfresco.rest.api.actions.ActionValidator.ALL_ACTIONS; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.repo.action.CompositeActionImpl; +import org.alfresco.rest.api.actions.ActionValidator; +import org.alfresco.rest.api.impl.rules.ActionParameterConverter; +import org.alfresco.rest.api.model.mapper.RestModelMapper; +import org.alfresco.rest.api.model.rules.Action; +import org.alfresco.service.Experimental; +import org.alfresco.util.GUID; +import org.apache.commons.collections.CollectionUtils; + +@Experimental +public class RestRuleActionModelMapper implements RestModelMapper +{ + private final ActionParameterConverter parameterConverter; + private final List actionValidators; + + public RestRuleActionModelMapper(ActionParameterConverter parameterConverter, + List actionValidators) + { + this.parameterConverter = parameterConverter; + this.actionValidators = actionValidators; + } + + /** + * Converts service POJO action to REST model action. + * + * @param actionModel - {@link org.alfresco.service.cmr.action.Action} service POJO + * @return {@link Action} REST model + */ + @Override + public Action toRestModel(org.alfresco.service.cmr.action.Action actionModel) + { + if (actionModel == null) + { + return null; + } + + final Action.Builder builder = Action.builder().actionDefinitionId(actionModel.getActionDefinitionName()); + if (actionModel.getParameterValues() != null) + { + final Map convertedParams = actionModel.getParameterValues() + .entrySet() + .stream() + .collect(HashMap::new, (m, v) -> m.put(v.getKey(), parameterConverter.convertParamFromServiceModel(v.getValue())), HashMap::putAll); + convertedParams.remove(ACTION_CONTEXT_PARAM_NAME); + builder.params(convertedParams); + } + return builder.create(); + } + + /** + * Convert the REST model objects to composite action service POJO. + * + * @param actions List of actions. + * @return The composite action service POJO. + */ + @Override + public org.alfresco.service.cmr.action.Action toServiceModel(Collection actions) + { + if (CollectionUtils.isEmpty(actions)) + { + return null; + } + + final org.alfresco.service.cmr.action.CompositeAction compositeAction = new CompositeActionImpl(null, GUID.generate()); + actions.forEach(action -> compositeAction.addAction(toServiceAction(action))); + return compositeAction; + } + + private org.alfresco.service.cmr.action.Action toServiceAction(Action action) + { + final Map params = Optional.ofNullable(action.getParams()).orElse(emptyMap()); + validateAction(action); + final Map convertedParams = + parameterConverter.getConvertedParams(params, action.getActionDefinitionId()); + return new ActionImpl(null, GUID.generate(), action.getActionDefinitionId(), convertedParams); + } + private void validateAction(Action action) { + actionValidators.stream() + .filter(v -> (v.getActionDefinitionIds().contains(action.getActionDefinitionId()) || + v.getActionDefinitionIds().equals(List.of(ALL_ACTIONS)))) + .sorted(Comparator.comparing(ActionValidator::getPriority)) + .forEachOrdered(v -> v.validate(action)); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleCompositeConditionModelMapper.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleCompositeConditionModelMapper.java new file mode 100644 index 0000000000..1783f74fd3 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleCompositeConditionModelMapper.java @@ -0,0 +1,162 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.mapper.rules; + +import static org.alfresco.repo.action.evaluator.NoConditionEvaluator.NAME; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.alfresco.rest.api.model.mapper.RestModelMapper; +import org.alfresco.rest.api.model.rules.CompositeCondition; +import org.alfresco.rest.api.model.rules.ConditionOperator; +import org.alfresco.rest.api.model.rules.SimpleCondition; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.action.ActionCondition; +import org.apache.commons.collections.CollectionUtils; + +@Experimental +public class RestRuleCompositeConditionModelMapper implements RestModelMapper +{ + private final RestModelMapper simpleConditionMapper; + + public RestRuleCompositeConditionModelMapper( + RestModelMapper simpleConditionMapper) + { + this.simpleConditionMapper = simpleConditionMapper; + } + + /** + * Converts Action conditions (service POJO) list to composite condition (REST model). + * + * @param actionConditions - list of {@link ActionCondition} service POJOs + * @return {@link CompositeCondition} REST model + */ + @Override + public CompositeCondition toRestModel(final Collection actionConditions) + { + if (CollectionUtils.isEmpty(actionConditions)) + { + return null; + } + final List filteredActions = actionConditions.stream() + .filter(Objects::nonNull) + .filter(c -> !NAME.equals(c.getActionConditionDefinitionName())) + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(filteredActions)) + { + return null; + } + final CompositeCondition conditions = new CompositeCondition(); + conditions.setCompositeConditions(new ArrayList<>()); + // group action conditions by inversion flag + filteredActions.stream() + .collect(Collectors.groupingBy(ActionCondition::getInvertCondition)) + // map action condition sub lists + .forEach((inverted, actionConditionsPart) -> Optional + .ofNullable(ofActionConditions(actionConditionsPart, inverted, ConditionOperator.AND)) + // if composite condition present add to final list + .ifPresent(compositeCondition -> conditions.getCompositeConditions().add(compositeCondition))); + + if (CollectionUtils.isEmpty(conditions.getCompositeConditions())) + { + conditions.setCompositeConditions(null); + } + return conditions; + } + + @Override + public List toServiceModels(final CompositeCondition compositeCondition) + { + final List actionConditions = new ArrayList<>(); + if (compositeCondition == null) + { + return actionConditions; + } + if (CollectionUtils.isNotEmpty(compositeCondition.getSimpleConditions())) + { + compositeCondition.getSimpleConditions() + .forEach(simpleCondition -> actionConditions.add(mapSimpleCondition(simpleCondition, compositeCondition.isInverted()))); + } + if (CollectionUtils.isNotEmpty(compositeCondition.getCompositeConditions())) + { + compositeCondition.getCompositeConditions().forEach(condition -> actionConditions.addAll(toServiceModels(condition))); + } + + return actionConditions; + } + + private ActionCondition mapSimpleCondition(final SimpleCondition simpleCondition, final boolean inverted) + { + final ActionCondition actionCondition = simpleConditionMapper.toServiceModel(simpleCondition); + actionCondition.setInvertCondition(inverted); + return actionCondition; + } + + private CompositeCondition ofActionConditions(final List actionConditions, final boolean inverted, + final ConditionOperator conditionOperator) + { + if (CollectionUtils.isEmpty(actionConditions)) + { + return null; + } + return ofSimpleConditions(simpleConditionMapper.toRestModels(actionConditions), inverted, conditionOperator); + } + + /** + * Creates a composite condition instance of simple conditions. + * + * @param simpleConditions - list of {@link SimpleCondition} + * @param inverted - determines if condition should be inverted + * @param conditionOperator - determines the operation, see {@link ConditionOperator} + * @return {@link CompositeCondition} + */ + private CompositeCondition ofSimpleConditions(final List simpleConditions, final boolean inverted, + final ConditionOperator conditionOperator) + { + return of(simpleConditions, null, inverted, conditionOperator); + } + + private CompositeCondition of(final List simpleConditions, final List compositeConditions, + final boolean inverted, final ConditionOperator conditionOperator) + { + if (CollectionUtils.isEmpty(simpleConditions) && CollectionUtils.isEmpty(compositeConditions)) + { + return null; + } + return CompositeCondition.builder() + .inverted(inverted) + .booleanMode(conditionOperator) + .simpleConditions(simpleConditions) + .compositeConditions(compositeConditions) + .create(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleModelMapper.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleModelMapper.java new file mode 100644 index 0000000000..192fb654ca --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleModelMapper.java @@ -0,0 +1,164 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.mapper.rules; + +import java.io.Serializable; +import java.util.Map; +import java.util.stream.Collectors; + +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.repo.action.executer.ScriptActionExecuter; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.impl.rules.ActionParameterConverter; +import org.alfresco.rest.api.model.mapper.RestModelMapper; +import org.alfresco.rest.api.model.rules.Action; +import org.alfresco.rest.api.model.rules.CompositeCondition; +import org.alfresco.rest.api.model.rules.Rule; +import org.alfresco.rest.api.model.rules.RuleTrigger; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.CompositeAction; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.GUID; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +@Experimental +public class RestRuleModelMapper implements RestModelMapper +{ + private static Log log = LogFactory.getLog(RestRuleModelMapper.class); + + private final RestModelMapper compositeConditionMapper; + private final RestModelMapper actionMapper; + private final Nodes nodes; + private final ActionParameterConverter actionParameterConverter; + + public RestRuleModelMapper( + RestModelMapper compositeConditionMapper, + RestModelMapper actionMapper, + Nodes nodes, + ActionParameterConverter actionParameterConverter) + { + this.compositeConditionMapper = compositeConditionMapper; + this.actionMapper = actionMapper; + this.nodes = nodes; + this.actionParameterConverter = actionParameterConverter; + } + + /** + * Converts service POJO rule to REST model rule. + * + * @param serviceRule - {@link org.alfresco.service.cmr.rule.Rule} service POJO + * @return {@link Rule} REST model + */ + @Override + public Rule toRestModel(org.alfresco.service.cmr.rule.Rule serviceRule) + { + if (serviceRule == null) + { + return null; + } + + final Rule.Builder builder = Rule.builder() + .name(serviceRule.getTitle()) + .description(serviceRule.getDescription()) + .isEnabled(!serviceRule.getRuleDisabled()) + .isInheritable(serviceRule.isAppliedToChildren()) + .isAsynchronous(serviceRule.getExecuteAsynchronously()); + + if (serviceRule.getNodeRef() != null) + { + builder.id(serviceRule.getNodeRef().getId()); + } + if (CollectionUtils.isNotEmpty(serviceRule.getRuleTypes())) + { + builder.triggers(serviceRule.getRuleTypes().stream().map(RuleTrigger::of).collect(Collectors.toList())); + } + if (serviceRule.getAction() != null) + { + builder.conditions(compositeConditionMapper.toRestModel(serviceRule.getAction().getActionConditions())); + if (serviceRule.getAction().getCompensatingAction() != null && + serviceRule.getAction().getCompensatingAction().getParameterValue(ScriptActionExecuter.PARAM_SCRIPTREF) != null) + { + String errorScript = actionParameterConverter.convertParamFromServiceModel( + serviceRule.getAction().getCompensatingAction().getParameterValue(ScriptActionExecuter.PARAM_SCRIPTREF)).toString(); + builder.errorScript(errorScript); + } + if (serviceRule.getAction() instanceof CompositeAction && ((CompositeAction) serviceRule.getAction()).getActions() != null) + { + builder.actions( + ((CompositeAction) serviceRule.getAction()).getActions().stream() + .map(actionMapper::toRestModel) + .collect(Collectors.toList())); + } else { + log.warn("Rule Action should be of 'CompositeAction' type but found: " + serviceRule.getAction().getClass()); + } + + } + + return builder.create(); + } + + /** + * Convert the REST model object to the equivalent service POJO. + * + * @param restRuleModel {@link Rule} REST model. + * @return The rule service POJO. + */ + @Override + public org.alfresco.service.cmr.rule.Rule toServiceModel(Rule restRuleModel) + { + final org.alfresco.service.cmr.rule.Rule serviceRule = new org.alfresco.service.cmr.rule.Rule(); + final NodeRef nodeRef = (restRuleModel.getId() != null) ? nodes.validateOrLookupNode(restRuleModel.getId(), null) : null; + serviceRule.setNodeRef(nodeRef); + serviceRule.setTitle(restRuleModel.getName()); + serviceRule.setDescription(restRuleModel.getDescription()); + serviceRule.setRuleDisabled(!restRuleModel.getIsEnabled()); + serviceRule.applyToChildren(restRuleModel.getIsInheritable()); + serviceRule.setExecuteAsynchronously(restRuleModel.getIsAsynchronous()); + serviceRule.setRuleTypes(restRuleModel.getTriggers()); + serviceRule.setAction(actionMapper.toServiceModel(restRuleModel.getActions())); + if (restRuleModel.getErrorScript() != null) + { + final org.alfresco.service.cmr.action.Action compensatingAction = + new ActionImpl(null, GUID.generate(), ScriptActionExecuter.NAME); + final Map scriptParam = actionParameterConverter + .getConvertedParams(Map.of(ScriptActionExecuter.PARAM_SCRIPTREF, restRuleModel.getErrorScript()), + compensatingAction.getActionDefinitionName()); + compensatingAction.setParameterValues(scriptParam); + serviceRule.getAction().setCompensatingAction(compensatingAction); + } + if (restRuleModel.getConditions() != null) + { + compositeConditionMapper.toServiceModels(restRuleModel.getConditions()) + .forEach(condition -> serviceRule.getAction().addActionCondition(condition)); + } + + return serviceRule; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleSimpleConditionModelMapper.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleSimpleConditionModelMapper.java new file mode 100644 index 0000000000..8f16fcb15c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleSimpleConditionModelMapper.java @@ -0,0 +1,233 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.mapper.rules; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import com.rometools.utils.Strings; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ActionConditionImpl; +import org.alfresco.repo.action.evaluator.CompareMimeTypeEvaluator; +import org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator; +import org.alfresco.repo.action.evaluator.HasAspectEvaluator; +import org.alfresco.repo.action.evaluator.HasTagEvaluator; +import org.alfresco.repo.action.evaluator.InCategoryEvaluator; +import org.alfresco.repo.action.evaluator.IsSubTypeEvaluator; +import org.alfresco.repo.action.evaluator.NoConditionEvaluator; +import org.alfresco.repo.action.evaluator.compare.ComparePropertyValueOperation; +import org.alfresco.repo.action.evaluator.compare.ContentPropertyName; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.mapper.RestModelMapper; +import org.alfresco.rest.api.model.rules.SimpleCondition; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.collections.MapUtils; + +@Experimental +public class RestRuleSimpleConditionModelMapper implements RestModelMapper +{ + static final String CATEGORY_INVALID_MSG = "Category in condition is invalid"; + static final String PARAM_CATEGORY = "category"; + static final String PARAM_MIMETYPE = "mimetype"; + static final String FIELD_NOT_NULL = "Field in condition must not be blank"; + static final String PARAMETER_NOT_NULL = "Parameter in condition must not be blank"; + static final String COMPARATOR_NOT_NULL = "Comparator in condition must not be blank"; + private final NamespaceService namespaceService; + private final Nodes nodes; + + public RestRuleSimpleConditionModelMapper(NamespaceService namespaceService, Nodes nodes) + { + this.namespaceService = namespaceService; + this.nodes = nodes; + } + + @Override + public SimpleCondition toRestModel(ActionCondition actionCondition) + { + if (actionCondition == null || actionCondition.getActionConditionDefinitionName() == null || + MapUtils.isEmpty(actionCondition.getParameterValues())) + { + return null; + } + + switch (actionCondition.getActionConditionDefinitionName()) + { + case ComparePropertyValueEvaluator.NAME: + return createComparePropertyValueCondition(actionCondition, namespaceService); + case CompareMimeTypeEvaluator.NAME: + return createCompareMimeTypeCondition(actionCondition); + case HasAspectEvaluator.NAME: + return createHasAspectCondition(actionCondition, namespaceService); + case HasTagEvaluator.NAME: + return createHasTagCondition(actionCondition); + case InCategoryEvaluator.NAME: + return createInCategoryCondition(actionCondition); + case IsSubTypeEvaluator.NAME: + return createIsSubtypeCondition(actionCondition, namespaceService); + case NoConditionEvaluator.NAME: + default: + return null; + } + } + + @Override + public ActionCondition toServiceModel(SimpleCondition restModel) + { + final String field = restModel.getField(); + checkStringNotBlank(field, FIELD_NOT_NULL); + + final Map parameterValues = new HashMap<>(); + String conditionDefinitionId; + final String parameter = restModel.getParameter(); + checkStringNotBlank(parameter, PARAMETER_NOT_NULL); + + switch (field) + { + case HasAspectEvaluator.PARAM_ASPECT: + conditionDefinitionId = HasAspectEvaluator.NAME; + parameterValues.put(HasAspectEvaluator.PARAM_ASPECT, QName.createQName(parameter, namespaceService)); + break; + case HasTagEvaluator.PARAM_TAG: + conditionDefinitionId = HasTagEvaluator.NAME; + parameterValues.put(HasTagEvaluator.PARAM_TAG, parameter); + break; + case PARAM_CATEGORY: + conditionDefinitionId = InCategoryEvaluator.NAME; + parameterValues.put(InCategoryEvaluator.PARAM_CATEGORY_ASPECT, ContentModel.ASPECT_GEN_CLASSIFIABLE); + try + { + parameterValues.put(InCategoryEvaluator.PARAM_CATEGORY_VALUE, nodes.validateOrLookupNode(parameter, null)); + } catch (EntityNotFoundException e) { + throw new InvalidArgumentException(CATEGORY_INVALID_MSG); + } + break; + case IsSubTypeEvaluator.PARAM_TYPE: + conditionDefinitionId = IsSubTypeEvaluator.NAME; + parameterValues.put(IsSubTypeEvaluator.PARAM_TYPE, QName.createQName(parameter, namespaceService)); + break; + case PARAM_MIMETYPE: + conditionDefinitionId = CompareMimeTypeEvaluator.NAME; + parameterValues.put(ComparePropertyValueEvaluator.PARAM_PROPERTY, ContentModel.TYPE_CONTENT); + parameterValues.put(ComparePropertyValueEvaluator.PARAM_VALUE, parameter); + break; + default: + conditionDefinitionId = ComparePropertyValueEvaluator.NAME; + try + { + // if size or encoding create content property evaluator + ContentPropertyName.valueOf(field.toUpperCase()); + parameterValues.put(ComparePropertyValueEvaluator.PARAM_CONTENT_PROPERTY, field.toUpperCase()); + parameterValues.put(ComparePropertyValueEvaluator.PARAM_PROPERTY, ContentModel.TYPE_CONTENT); + } + catch (IllegalArgumentException ignore) + { + // else create common property evaluator + parameterValues.put(ComparePropertyValueEvaluator.PARAM_PROPERTY, QName.createQName(field, namespaceService)); + } + checkStringNotBlank(restModel.getComparator(), COMPARATOR_NOT_NULL); + parameterValues.put(ComparePropertyValueEvaluator.PARAM_OPERATION, restModel.getComparator().toUpperCase()); + parameterValues.put(ComparePropertyValueEvaluator.PARAM_VALUE, parameter); + break; + } + return new ActionConditionImpl(UUID.randomUUID().toString(), conditionDefinitionId, parameterValues); + } + + private void checkStringNotBlank(final String string, final String message) { + if (Strings.isBlank(string)) + { + throw new InvalidArgumentException(message); + } + } + + private static SimpleCondition createComparePropertyValueCondition(final ActionCondition actionCondition, final NamespaceService namespaceService) + { + final SimpleCondition.Builder builder = SimpleCondition.builder(); + if (actionCondition.getParameterValues().get(ComparePropertyValueEvaluator.PARAM_CONTENT_PROPERTY) != null) + { + builder.field(actionCondition.getParameterValues().get(ComparePropertyValueEvaluator.PARAM_CONTENT_PROPERTY).toString().toLowerCase()); + } else { + builder.field(((QName) actionCondition.getParameterValues().get(ComparePropertyValueEvaluator.PARAM_PROPERTY)).toPrefixString(namespaceService)); + } + return builder + .comparator(actionCondition.getParameterValues().get(ComparePropertyValueEvaluator.PARAM_OPERATION).toString().toLowerCase()) + .parameter(actionCondition.getParameterValues().get(ComparePropertyValueEvaluator.PARAM_VALUE).toString()) + .create(); + } + + private static SimpleCondition createCompareMimeTypeCondition(final ActionCondition actionCondition) + { + return SimpleCondition.builder() + .field(PARAM_MIMETYPE) + .comparator(ComparePropertyValueOperation.EQUALS.toString().toLowerCase()) + .parameter(actionCondition.getParameterValues().get(ComparePropertyValueEvaluator.PARAM_VALUE).toString()) + .create(); + } + + private static SimpleCondition createHasAspectCondition(final ActionCondition actionCondition, final NamespaceService namespaceService) + { + return SimpleCondition.builder() + .field(HasAspectEvaluator.PARAM_ASPECT) + .comparator(ComparePropertyValueOperation.EQUALS.toString().toLowerCase()) + .parameter(((QName) actionCondition.getParameterValues().get(HasAspectEvaluator.PARAM_ASPECT)).toPrefixString(namespaceService)) + .create(); + } + + private static SimpleCondition createHasTagCondition(final ActionCondition actionCondition) + { + return SimpleCondition.builder() + .field(HasTagEvaluator.PARAM_TAG) + .comparator(ComparePropertyValueOperation.EQUALS.toString().toLowerCase()) + .parameter(actionCondition.getParameterValues().get(HasTagEvaluator.PARAM_TAG).toString()) + .create(); + } + + private static SimpleCondition createInCategoryCondition(final ActionCondition actionCondition) + { + return SimpleCondition.builder() + .field(PARAM_CATEGORY) + .comparator(ComparePropertyValueOperation.EQUALS.toString().toLowerCase()) + .parameter(((NodeRef) actionCondition.getParameterValues().get(InCategoryEvaluator.PARAM_CATEGORY_VALUE)).getId()) + .create(); + } + + private static SimpleCondition createIsSubtypeCondition(final ActionCondition actionCondition, final NamespaceService namespaceService) + { + return SimpleCondition.builder() + .field(IsSubTypeEvaluator.PARAM_TYPE) + .comparator(ComparePropertyValueOperation.EQUALS.toString().toLowerCase()) + .parameter(((QName) actionCondition.getParameterValues().get(IsSubTypeEvaluator.PARAM_TYPE)).toPrefixString(namespaceService)) + .create(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/ActionParameterConverter.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/ActionParameterConverter.java new file mode 100644 index 0000000000..d849bb7e24 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/ActionParameterConverter.java @@ -0,0 +1,190 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.rules; + +import static org.alfresco.rest.framework.core.exceptions.NotFoundException.DEFAULT_MESSAGE_ID; +import static org.alfresco.service.cmr.security.AccessStatus.ALLOWED; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.action.ParameterizedItemDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryException; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.logging.log4j.util.Strings; +import org.json.JSONArray; +import org.json.JSONException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; + +@Experimental +public class ActionParameterConverter +{ + static final String ACTION_PARAMETER_SHOULD_NOT_HAVE_EMPTY_OR_NULL_VALUE = + "Action parameter should not have empty or null value"; + private final DictionaryService dictionaryService; + private final ActionService actionService; + private final NamespaceService namespaceService; + private final PermissionService permissionService; + private final Nodes nodes; + + public ActionParameterConverter(DictionaryService dictionaryService, ActionService actionService, NamespaceService namespaceService, + PermissionService permissionService, Nodes nodes) + { + this.dictionaryService = dictionaryService; + this.actionService = actionService; + this.namespaceService = namespaceService; + this.permissionService = permissionService; + this.nodes = nodes; + } + + public Map getConvertedParams(Map params, String name) + { + final Map parameters = new HashMap<>(params.size()); + final ParameterizedItemDefinition definition; + try + { + definition = actionService.getActionDefinition(name); + if (definition == null) + { + throw new NotFoundException(DEFAULT_MESSAGE_ID, new String[]{name}); + } + } + catch (NoSuchBeanDefinitionException e) + { + throw new NotFoundException(DEFAULT_MESSAGE_ID, new String[]{name}); + } + + for (Map.Entry param : params.entrySet()) + { + if (Objects.toString(param.getValue(), Strings.EMPTY).isEmpty()) { + throw new InvalidArgumentException(ACTION_PARAMETER_SHOULD_NOT_HAVE_EMPTY_OR_NULL_VALUE, new String[] {param.getKey()}); + } + final ParameterDefinition paramDef = definition.getParameterDefintion(param.getKey()); + if (paramDef == null && !definition.getAdhocPropertiesAllowed()) + { + throw new InvalidArgumentException(InvalidArgumentException.DEFAULT_MESSAGE_ID, new String[]{param.getKey(), name}); + } + if (paramDef != null) + { + final QName typeQName = paramDef.getType(); + parameters.put(param.getKey(), convertValue(typeQName, param.getValue())); + } + else + { + parameters.put(param.getKey(), param.getValue().toString()); + } + } + return parameters; + } + + public Serializable convertParamFromServiceModel(Serializable param) + { + if (param instanceof QName) + { + return ((QName) param).toPrefixString(namespaceService); + } + else if (param instanceof NodeRef) + { + return ((NodeRef) param).getId(); + } + else + { + return param; + } + } + + private Serializable convertValue(QName typeQName, Object propertyValue) throws JSONException + { + Serializable value; + + final DataTypeDefinition typeDef = dictionaryService.getDataType(typeQName); + if (typeDef == null) + { + throw new NotFoundException(DEFAULT_MESSAGE_ID, new String[]{typeQName.toPrefixString()}); + } + + if (propertyValue instanceof JSONArray) + { + final String javaClassName = typeDef.getJavaClassName(); + try + { + Class.forName(javaClassName); + } + catch (ClassNotFoundException e) + { + throw new DictionaryException("Java class " + javaClassName + " of property type " + typeDef.getName() + " is invalid", e); + } + + final int length = ((JSONArray) propertyValue).length(); + final List list = new ArrayList<>(length); + for (int i = 0; i < length; i++) + { + list.add(convertValue(typeQName, ((JSONArray) propertyValue).get(i))); + } + value = (Serializable) list; + } + else + { + final String stringValue = Objects.toString(propertyValue, Strings.EMPTY); + if (typeQName.isMatch(DataTypeDefinition.QNAME) && typeQName.toString().contains(":")) + { + value = QName.createQName(stringValue, namespaceService); + } + else if (typeQName.isMatch(DataTypeDefinition.NODE_REF)) + { + NodeRef nodeRef = nodes.validateOrLookupNode(stringValue, null); + if (permissionService.hasReadPermission(nodeRef) != ALLOWED) + { + throw new EntityNotFoundException(stringValue); + } + value = nodeRef; + } + else + { + value = (Serializable) DefaultTypeConverter.INSTANCE.convert(dictionaryService.getDataType(typeQName), propertyValue); + } + } + return value; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/ActionPermissionValidator.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/ActionPermissionValidator.java new file mode 100644 index 0000000000..d17efe05e5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/ActionPermissionValidator.java @@ -0,0 +1,85 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.rules; + +import static org.alfresco.repo.web.scripts.rule.AbstractRuleWebScript.CANNOT_CREATE_RULE; +import static org.alfresco.service.cmr.rule.RuleType.OUTBOUND; + +import java.util.Collections; +import java.util.List; + +import org.alfresco.repo.action.CompositeActionImpl; +import org.alfresco.repo.action.RuntimeActionService; +import org.alfresco.repo.action.access.ActionAccessRestriction; +import org.alfresco.repo.action.executer.CheckOutActionExecuter; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.rule.Rule; +import org.apache.commons.collections.CollectionUtils; + +@Experimental +public class ActionPermissionValidator +{ + private final RuntimeActionService runtimeActionService; + + public ActionPermissionValidator(RuntimeActionService runtimeActionService) + { + this.runtimeActionService = runtimeActionService; + } + + Rule validateRulePermissions(Rule rule) + { + final List actions = ((CompositeActionImpl) rule.getAction()).getActions(); + + checkRestrictedAccessActions(actions); + checkRuleOutboundHasNoCheckOutAction(rule, actions); + return rule; + } + + private void checkRestrictedAccessActions(List actions) { + actions.forEach(action -> { + ActionAccessRestriction.setActionContext(action, ActionAccessRestriction.RULE_ACTION_CONTEXT); + runtimeActionService.verifyActionAccessRestrictions(action); + }); + } + + private void checkRuleOutboundHasNoCheckOutAction(Rule rule, List actions) { + //TODO: rule types should never be empty in final implementation + if (CollectionUtils.isNotEmpty(rule.getRuleTypes()) && rule.getRuleTypes().contains(OUTBOUND)) + { + for (Action action : actions) + { + if (action.getActionDefinitionName().equalsIgnoreCase(CheckOutActionExecuter.NAME)) + { + throw new InvalidArgumentException(CANNOT_CREATE_RULE); + } + } + } + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/NodeValidator.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/NodeValidator.java new file mode 100644 index 0000000000..31b3fdf6a6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/NodeValidator.java @@ -0,0 +1,241 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.impl.rules; + +import static org.alfresco.service.cmr.security.AccessStatus.ALLOWED; +import static org.alfresco.service.cmr.security.PermissionService.CHANGE_PERMISSIONS; + +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.rule.RuleModel; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.api.model.rules.RuleSet; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundException; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.QName; + +/** Responsible for validating nodes when working with rules. */ +@Experimental +public class NodeValidator +{ + private static final String RULE_SET_EXPECTED_TYPE_NAME = "rule set"; + + private Nodes nodes; + private RuleService ruleService; + private PermissionService permissionService; + private NodeService nodeService; + + /** + * Validates if folder node exists and the user has permission to use it. + * + * @param folderNodeId - folder node ID + * @param requireChangePermission - Whether to require change permission or just read permission. + * @return folder node reference + * @throws InvalidArgumentException if node is not of an expected type + * @throws PermissionDeniedException if the user doesn't have the appropriate permission for the folder. + * @throws EntityNotFoundException if the folder node isn't found + */ + public NodeRef validateFolderNode(final String folderNodeId, boolean requireChangePermission) + { + try + { + final NodeRef nodeRef = nodes.validateOrLookupNode(folderNodeId, null); + validatePermission(requireChangePermission, nodeRef); + verifyNodeType(nodeRef, ContentModel.TYPE_FOLDER, null); + + return nodeRef; + } catch (EntityNotFoundException e) + { + throw new EntityNotFoundException("Folder with id " + folderNodeId + " was not found.", e); + } + } + + /** + * Validates if rule set ID is default, node exists and associated folder node matches. + * + * @param ruleSetId - rule set node ID + * @param associatedFolderNodeRef - folder node ref to check the association + * @return rule set node reference + * @throws InvalidArgumentException in case of not matching associated folder node + * @throws RelationshipResourceNotFoundException if the folder doesn't have a -default- rule set + * @throws EntityNotFoundException if the rule set node isn't found + */ + public NodeRef validateRuleSetNode(final String ruleSetId, final NodeRef associatedFolderNodeRef) + { + if (RuleSet.isDefaultId(ruleSetId)) + { + final NodeRef ruleSetNodeRef = ruleService.getRuleSetNode(associatedFolderNodeRef); + if (ruleSetNodeRef == null) + { + //folder doesn't have a -default- rule set + throw new RelationshipResourceNotFoundException(associatedFolderNodeRef.getId(), ruleSetId); + } + return ruleSetNodeRef; + } + + try { + final NodeRef ruleSetNodeRef = validateNode(ruleSetId, ContentModel.TYPE_SYSTEM_FOLDER, RULE_SET_EXPECTED_TYPE_NAME); + + if (!ruleService.isRuleSetAssociatedWithFolder(ruleSetNodeRef, associatedFolderNodeRef)) + { + throw new InvalidArgumentException("Rule set is not associated with folder node!"); + } + return ruleSetNodeRef; + + } catch (EntityNotFoundException e) { + throw new EntityNotFoundException("Rule set with id " + ruleSetId + " was not found.", e); + } + } + + public NodeRef validateRuleSetNode(String linkToNodeId, boolean requireChangePermission) + { + final Node ruleSetNode = nodes.getNode(linkToNodeId); + final ChildAssociationRef primaryParent = nodeService.getPrimaryParent(ruleSetNode.getNodeRef()); + final NodeRef parentNode = primaryParent.getParentRef(); + validatePermission(requireChangePermission, parentNode); + return parentNode; + } + + + /** + * Validates if rule node exists and associated rule set node matches. + * + * @param ruleId - rule node ID + * @param associatedRuleSetNodeRef - rule set node ref to check the association. Can be null + * @return rule node reference + * @throws InvalidArgumentException in case of not matching associated rule set node + */ + public NodeRef validateRuleNode(final String ruleId, final NodeRef associatedRuleSetNodeRef) + { + final NodeRef ruleNodeRef = validateNode(ruleId, RuleModel.TYPE_RULE, null); + if (associatedRuleSetNodeRef != null && !ruleService.isRuleAssociatedWithRuleSet(ruleNodeRef, associatedRuleSetNodeRef)) + { + throw new InvalidArgumentException("Rule is not associated with rule set node!"); + } + + return ruleNodeRef; + } + + private NodeRef validateNode(final String nodeId, final QName expectedType, final String expectedTypeName) + { + final NodeRef nodeRef = nodes.validateNode(nodeId); + verifyNodeType(nodeRef, expectedType, expectedTypeName); + + return nodeRef; + } + + private void validatePermission(boolean requireChangePermission, NodeRef nodeRef) + { + if (requireChangePermission) + { + if (permissionService.hasPermission(nodeRef, CHANGE_PERMISSIONS) != ALLOWED) + { + throw new PermissionDeniedException("Insufficient permissions to manage rules."); + } + } + else + { + if (permissionService.hasReadPermission(nodeRef) != ALLOWED) + { + throw new PermissionDeniedException("Cannot read from this node!"); + } + } + } + + private void verifyNodeType(final NodeRef nodeRef, final QName expectedType, final String expectedTypeName) + { + final Set expectedTypes = Set.of(expectedType); + if (!nodes.nodeMatches(nodeRef, expectedTypes, null)) + { + final String expectedTypeLocalName = (expectedTypeName != null) ? expectedTypeName : expectedType.getLocalName(); + throw new InvalidArgumentException(String.format("NodeId of a %s is expected!", expectedTypeLocalName)); + } + } + + public boolean isRuleSetNode(String nodeId) { + try + { + validateNode(nodeId, ContentModel.TYPE_SYSTEM_FOLDER, RULE_SET_EXPECTED_TYPE_NAME); + return true; + } catch (InvalidArgumentException e) { + return false; + } + } + + /** + * Verifies if rule set node or folder node's default rule set is shared + * @param ruleSetNodeRef + * @param folderNodeRef + * @return + */ + public boolean isRuleSetNotNullAndShared(final NodeRef ruleSetNodeRef, final NodeRef folderNodeRef) + { + if (ruleSetNodeRef == null && folderNodeRef != null) + { + final NodeRef ruleSetNode = ruleService.getRuleSetNode(folderNodeRef); + return ruleSetNode != null && ruleService.isRuleSetShared(ruleSetNode); + } + else + { + return isRuleSetNotNullAndShared(ruleSetNodeRef); + } + } + + public boolean isRuleSetNotNullAndShared(final NodeRef ruleSetNodeRef) + { + return ruleSetNodeRef != null && ruleService.isRuleSetShared(ruleSetNodeRef); + } + + public void setPermissionService(PermissionService permissionService) + { + this.permissionService = permissionService; + } + + public void setRuleService(RuleService ruleService) + { + this.ruleService = ruleService; + } + + public void setNodes(Nodes nodes) + { + this.nodes = nodes; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleLoader.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleLoader.java new file mode 100644 index 0000000000..aff384709e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleLoader.java @@ -0,0 +1,75 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.impl.rules; + +import java.util.List; + +import org.alfresco.rest.api.model.mapper.RestModelMapper; +import org.alfresco.rest.api.model.rules.CompositeCondition; +import org.alfresco.rest.api.model.rules.Rule; +import org.alfresco.rest.api.model.rules.SimpleCondition; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.RuleService; + +/** Responsible for creating {@link Rule} objects. */ +@Experimental +public class RuleLoader +{ + public static final String IS_SHARED = "isShared"; + private RuleService ruleService; + private NodeValidator nodeValidator; + private RestModelMapper ruleMapper; + + public Rule loadRule(org.alfresco.service.cmr.rule.Rule ruleModel, List includes) + { + final Rule rule = ruleMapper.toRestModel(ruleModel); + if (includes != null && includes.contains(IS_SHARED)) + { + NodeRef ruleSet = ruleService.getRuleSetNode(ruleModel.getNodeRef()); + boolean isShared = nodeValidator.isRuleSetNotNullAndShared(ruleSet); + rule.setIsShared(isShared); + } + return rule; + } + + public void setRuleService(RuleService ruleService) + { + this.ruleService = ruleService; + } + + public void setNodeValidator(NodeValidator nodeValidator) + { + this.nodeValidator = nodeValidator; + } + + public void setRuleMapper( + RestModelMapper ruleMapper) + { + this.ruleMapper = ruleMapper; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSetLoader.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSetLoader.java new file mode 100644 index 0000000000..9ded55ecc4 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSetLoader.java @@ -0,0 +1,169 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.impl.rules; + +import static java.util.stream.Collectors.toList; + +import static org.alfresco.rest.api.model.rules.InclusionType.INHERITED; +import static org.alfresco.rest.api.model.rules.InclusionType.LINKED; +import static org.alfresco.rest.api.model.rules.InclusionType.OWNED; + +import java.util.List; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.rest.api.impl.mapper.rules.RestRuleModelMapper; +import org.alfresco.rest.api.model.rules.RuleSet; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.rule.RuleService; + +/** Responsible for converting a NodeRef into a {@link RuleSet} object. */ +@Experimental +public class RuleSetLoader +{ + protected static final String OWNING_FOLDER = "owningFolder"; + protected static final String INCLUSION_TYPE = "inclusionType"; + protected static final String INHERITED_BY = "inheritedBy"; + protected static final String LINKED_TO_BY = "linkedToBy"; + protected static final String IS_INHERITED = "isInherited"; + protected static final String IS_LINKED_TO = "isLinkedTo"; + protected static final String RULE_IDS = "ruleIds"; + private static final int MAX_INHERITED_BY_SIZE = 100; + private static final int MAX_LINKED_TO_BY_SIZE = 100; + private NodeService nodeService; + private RuleService ruleService; + + /** + * Load a rule set for the given node ref. + * + * @param ruleSetNodeRef The rule set node. + * @param includes A list of fields to include. + * @return The rule set object. + */ + public RuleSet loadRuleSet(NodeRef ruleSetNodeRef, NodeRef folderNodeRef, List includes) + { + String ruleSetId = ruleSetNodeRef.getId(); + RuleSet ruleSet = RuleSet.of(ruleSetId); + + if (includes != null) + { + NodeRef parentRef = nodeService.getPrimaryParent(ruleSetNodeRef).getParentRef(); + if (includes.contains(OWNING_FOLDER)) + { + ruleSet.setOwningFolder(parentRef); + } + if (includes.contains(INCLUSION_TYPE)) + { + // In the case that a rule set applies to the given folder for multiple reasons then priority is given to owned, then linked, then inherited. + if (parentRef.equals(folderNodeRef)) + { + ruleSet.setInclusionType(OWNED); + } + else + { + boolean linked = nodeService.getParentAssocs(ruleSetNodeRef) + .stream().map(ChildAssociationRef::getParentRef) + .anyMatch(folderNodeRef::equals); + ruleSet.setInclusionType(linked ? LINKED : INHERITED); + } + } + if (includes.contains(INHERITED_BY)) + { + ruleSet.setInheritedBy(loadInheritedBy(ruleSetNodeRef)); + } + if (includes.contains(LINKED_TO_BY)) + { + ruleSet.setLinkedToBy(loadLinkedToBy(ruleSetNodeRef)); + } + if (includes.contains(IS_INHERITED)) + { + ruleSet.setIsInherited(loadIsInherited(ruleSetNodeRef)); + } + if (includes.contains(IS_LINKED_TO)) + { + ruleSet.setIsLinkedTo(loadIsLinkedTo(ruleSetNodeRef, parentRef)); + } + if (includes.contains(RULE_IDS)) + { + ruleSet.setRuleIds(loadRuleIds(parentRef)); + } + } + return ruleSet; + } + + private List loadInheritedBy(NodeRef ruleSetNodeRef) + { + return ruleService.getFoldersInheritingRuleSet(ruleSetNodeRef, MAX_INHERITED_BY_SIZE); + } + + private List loadLinkedToBy(NodeRef ruleSetNodeRef) + { + return ruleService.getFoldersLinkingToRuleSet(ruleSetNodeRef, MAX_LINKED_TO_BY_SIZE); + } + + private boolean loadIsInherited(NodeRef ruleSetNodeRef) + { + return AuthenticationUtil.runAsSystem(() -> !ruleService.getFoldersInheritingRuleSet(ruleSetNodeRef, 1).isEmpty()); + } + + /** + * Check if any parents of the rule set node are not the owning folder. + * + * @param ruleSetNodeRef The rule set node. + * @param parentRef The owning folder. + * @return True if another folder links to the rule set. + */ + private Boolean loadIsLinkedTo(NodeRef ruleSetNodeRef, NodeRef parentRef) + { + return AuthenticationUtil.runAsSystem(() -> + nodeService.getParentAssocs(ruleSetNodeRef) + .stream() + .map(ChildAssociationRef::getParentRef) + .anyMatch(folder -> !folder.equals(parentRef)) + ); + } + + public List loadRuleIds(NodeRef folderNodeRef) + { + return ruleService.getRules(folderNodeRef, false).stream() + .map(org.alfresco.service.cmr.rule.Rule::getNodeRef) + .map(NodeRef::getId) + .collect(toList()); + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setRuleService(RuleService ruleService) + { + this.ruleService = ruleService; + } + +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSetsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSetsImpl.java new file mode 100644 index 0000000000..4c99e81225 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSetsImpl.java @@ -0,0 +1,228 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.rules; + +import static java.util.stream.Collectors.toList; + +import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE; +import static org.alfresco.util.collections.CollectionUtils.isEmpty; + +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.IntStream; + +import org.alfresco.repo.rule.RuleModel; +import org.alfresco.repo.rule.RuntimeRuleService; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.rest.api.RuleSets; +import org.alfresco.rest.api.model.rules.RuleSet; +import org.alfresco.rest.api.model.rules.RuleSetLink; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.ListPage; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.rule.RuleService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Experimental +public class RuleSetsImpl implements RuleSets +{ + private static final Logger LOGGER = LoggerFactory.getLogger(RuleSetsImpl.class); + + private RuleSetLoader ruleSetLoader; + private RuleService ruleService; + private NodeValidator validator; + private NodeService nodeService; + private RuntimeRuleService runtimeRuleService; + + @Override + public CollectionWithPagingInfo getRuleSets(String folderNodeId, List includes, Paging paging) + { + NodeRef folderNode = validator.validateFolderNode(folderNodeId, false); + + List ruleSets = ruleService.getNodesSupplyingRuleSets(folderNode) + .stream() + .map(supplyingNode -> loadRuleSet(supplyingNode, folderNode, includes)) + .filter(Objects::nonNull) + .distinct() + .collect(toList()); + + return ListPage.of(ruleSets, paging); + } + + /** + * Load the specified rule set if the user has permission. + * + * @param supplyingNode The folder supplying a rule set. + * @param folderNode The folder being supplied with rule sets. + * @param includes The list of optional fields to include for each rule set in the response. + * @return The rule set from the DB or null if the folder has no rule set, or the current user does not have permission to view it. + */ + private RuleSet loadRuleSet(NodeRef supplyingNode, NodeRef folderNode, List includes) + { + NodeRef ruleSetNode = ruleService.getRuleSetNode(supplyingNode); + // Check if the folder has no rule sets. + if (ruleSetNode == null) + { + return null; + } + + try + { + return ruleSetLoader.loadRuleSet(ruleSetNode, folderNode, includes); + } + catch (AccessDeniedException e) + { + LOGGER.debug("User does not have permission to view rule set with id {}.", ruleSetNode, e); + return null; + } + } + + @Override + public RuleSet getRuleSetById(String folderNodeId, String ruleSetId, List includes) + { + NodeRef folderNode = validator.validateFolderNode(folderNodeId, false); + NodeRef ruleSetNode = validator.validateRuleSetNode(ruleSetId, folderNode); + + return ruleSetLoader.loadRuleSet(ruleSetNode, folderNode, includes); + } + + @Override + public RuleSet updateRuleSet(String folderNodeId, RuleSet ruleSet, List includes) + { + // Editing the order of the rules doesn't require permission to edit the rule set itself. + NodeRef folderNode = validator.validateFolderNode(folderNodeId, false); + NodeRef ruleSetNode = validator.validateRuleSetNode(ruleSet.getId(), folderNode); + + RuleSet returnedRuleSet = ruleSetLoader.loadRuleSet(ruleSetNode, folderNode, includes); + + // Currently the only field that can be updated is ruleIds to reorder the rules. + List suppliedRuleIds = ruleSet.getRuleIds(); + if (!isEmpty(suppliedRuleIds)) + { + // Check there are no duplicate rule ids in the request. + Set suppliedRuleIdSet = new HashSet<>(suppliedRuleIds); + + // Check that the set of rule ids hasn't changed. + Set existingRuleIds = new HashSet<>(ruleSetLoader.loadRuleIds(folderNode)); + if (suppliedRuleIdSet.size() != suppliedRuleIds.size() || !suppliedRuleIdSet.equals(existingRuleIds)) + { + throw new InvalidArgumentException("Unexpected set of rule ids - received " + suppliedRuleIds + " but expected " + existingRuleIds); + } + + IntStream.range(0, suppliedRuleIds.size()).forEach(index -> + { + NodeRef ruleNode = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, suppliedRuleIds.get(index)); + ruleService.setRulePosition(folderNode, ruleNode, index); + }); + if (includes.contains(RuleSetLoader.RULE_IDS)) + { + returnedRuleSet.setRuleIds(suppliedRuleIds); + } + } + + return returnedRuleSet; + } + + @Override + public RuleSetLink linkToRuleSet(String folderNodeId, String linkToNodeId) + { + + final NodeRef folderNodeRef = validator.validateFolderNode(folderNodeId,true); + final boolean isRuleSetNode = validator.isRuleSetNode(linkToNodeId); + final NodeRef linkToNodeRef = isRuleSetNode + ? validator.validateRuleSetNode(linkToNodeId, false) + : validator.validateFolderNode(linkToNodeId, false); + + //The target node should have pre-existing rules to link to + if (!ruleService.hasRules(linkToNodeRef)) { + throw new InvalidArgumentException("The target node has no rules to link."); + } + + //The folder shouldn't have any pre-existing rules + if (ruleService.hasRules(folderNodeRef)) { + throw new InvalidArgumentException("Unable to link to a rule set because the folder has pre-existing rules or is already linked to a rule set."); + } + + // Create the destination folder as a secondary child of the first + NodeRef ruleSetNodeRef = runtimeRuleService.getSavedRuleFolderAssoc(linkToNodeRef).getChildRef(); + // The required aspect will automatically be added to the node + nodeService.addChild(folderNodeRef, ruleSetNodeRef, RuleModel.ASSOC_RULE_FOLDER, RuleModel.ASSOC_RULE_FOLDER); + + RuleSetLink ruleSetLink = new RuleSetLink(); + ruleSetLink.setId(ruleSetNodeRef.getId()); + + return ruleSetLink; + } + + @Override + public void unlinkRuleSet(String folderNodeId, String ruleSetId) + { + final NodeRef folderNodeRef = validator.validateFolderNode(folderNodeId,true); + final NodeRef ruleSetNodeRef = validator.validateRuleSetNode(ruleSetId, folderNodeRef); + + //The folder should be linked to a rule set + if (!ruleService.isLinkedToRuleNode(folderNodeRef)) + { + throw new InvalidArgumentException("The folder is not linked to a rule set."); + } + + //The following line also handles the deletion of the parent-child association that gets created during linking + nodeService.removeAspect(folderNodeRef,RuleModel.ASPECT_RULES); + } + + public void setRuleSetLoader(RuleSetLoader ruleSetLoader) + { + this.ruleSetLoader = ruleSetLoader; + } + + public void setValidator(NodeValidator validator) + { + this.validator = validator; + } + + public void setRuleService(RuleService ruleService) + { + this.ruleService = ruleService; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setRuntimeRuleService(RuntimeRuleService runtimeRuleService) + { + this.runtimeRuleService = runtimeRuleService; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSettingsImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSettingsImpl.java new file mode 100644 index 0000000000..081bd8732b --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RuleSettingsImpl.java @@ -0,0 +1,107 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.impl.rules; + +import static org.alfresco.repo.rule.RuleModel.ASPECT_IGNORE_INHERITED_RULES; +import static org.alfresco.rest.api.model.rules.RuleSetting.IS_INHERITANCE_ENABLED_KEY; + +import java.util.Collections; + +import org.alfresco.rest.api.RuleSettings; +import org.alfresco.rest.api.model.rules.RuleSetting; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; + +@Experimental +public class RuleSettingsImpl implements RuleSettings +{ + private NodeValidator validator; + private NodeService nodeService; + + @Override + public RuleSetting getRuleSetting(String folderId, String ruleSettingKey) + { + NodeRef folderNode = validator.validateFolderNode(folderId, false); + switch (ruleSettingKey) + { + case IS_INHERITANCE_ENABLED_KEY: + return getIsInheritanceEnabled(folderNode); + default: + throw new NotFoundException("Unrecognised rule setting key " + ruleSettingKey); + } + } + + private RuleSetting getIsInheritanceEnabled(NodeRef folderNode) + { + boolean inheritanceDisabled = nodeService.hasAspect(folderNode, ASPECT_IGNORE_INHERITED_RULES); + return RuleSetting.builder().key(IS_INHERITANCE_ENABLED_KEY).value(!inheritanceDisabled).create(); + } + + @Override + public RuleSetting setRuleSetting(String folderId, RuleSetting ruleSetting) + { + NodeRef folderNode = validator.validateFolderNode(folderId, true); + + switch (ruleSetting.getKey()) + { + case IS_INHERITANCE_ENABLED_KEY: + return updateIsInheritanceEnabled(folderNode, ruleSetting.getValue()); + default: + throw new NotFoundException("Unrecognised rule setting key " + ruleSetting.getKey()); + } + } + + private RuleSetting updateIsInheritanceEnabled(NodeRef folderNode, Object value) + { + if (!(value instanceof Boolean)) + { + throw new IllegalArgumentException("Rule setting " + IS_INHERITANCE_ENABLED_KEY + " requires a boolean value."); + } + + if ((boolean) value) + { + nodeService.removeAspect(folderNode, ASPECT_IGNORE_INHERITED_RULES); + } + else + { + nodeService.addAspect(folderNode, ASPECT_IGNORE_INHERITED_RULES, Collections.emptyMap()); + } + + return RuleSetting.builder().key(IS_INHERITANCE_ENABLED_KEY).value(value).create(); + } + + public void setValidator(NodeValidator validator) + { + this.validator = validator; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RulesImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RulesImpl.java new file mode 100644 index 0000000000..160a783209 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/rules/RulesImpl.java @@ -0,0 +1,195 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.rules; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.repo.action.access.ActionAccessRestriction; +import org.alfresco.repo.action.executer.ExecuteAllRulesActionExecuter; +import org.alfresco.rest.api.Rules; +import org.alfresco.rest.api.model.mapper.RestModelMapper; +import org.alfresco.rest.api.model.rules.Rule; +import org.alfresco.rest.api.model.rules.RuleExecution; +import org.alfresco.rest.api.model.rules.RuleSet; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.ListPage; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.util.GUID; +import org.apache.commons.collections.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Experimental +public class RulesImpl implements Rules +{ + private static final Logger LOGGER = LoggerFactory.getLogger(RulesImpl.class); + private static final String MUST_HAVE_AT_LEAST_ONE_ACTION = "A rule must have at least one action"; + + private ActionService actionService; + private RuleService ruleService; + private NodeValidator validator; + private RuleLoader ruleLoader; + private ActionPermissionValidator actionPermissionValidator; + private RestModelMapper ruleMapper; + + @Override + public CollectionWithPagingInfo getRules(final String folderNodeId, + final String ruleSetId, + final List includes, + final Paging paging) + { + final NodeRef folderNodeRef = validator.validateFolderNode(folderNodeId, false); + NodeRef ruleSetNode = validator.validateRuleSetNode(ruleSetId, folderNodeRef); + NodeRef owningFolder = ruleService.getOwningNodeRef(ruleSetNode); + + final List rules = ruleService.getRules(owningFolder, false).stream() + .map(ruleModel -> ruleLoader.loadRule(ruleModel, includes)) + .collect(Collectors.toList()); + + return ListPage.of(rules, paging); + } + + @Override + public Rule getRuleById(final String folderNodeId, final String ruleSetId, final String ruleId, final List includes) + { + final NodeRef folderNodeRef = validator.validateFolderNode(folderNodeId, false); + final NodeRef ruleSetNodeRef = validator.validateRuleSetNode(ruleSetId, folderNodeRef); + final NodeRef ruleNodeRef = validator.validateRuleNode(ruleId, ruleSetNodeRef); + + return ruleLoader.loadRule(ruleService.getRule(ruleNodeRef), includes); + } + + @Override + public List createRules(final String folderNodeId, final String ruleSetId, final List rules, final List includes) + { + final NodeRef folderNodeRef = validator.validateFolderNode(folderNodeId, true); + // Don't validate the ruleset node if -default- is passed since we may need to create it. + if (RuleSet.isNotDefaultId(ruleSetId)) + { + validator.validateRuleSetNode(ruleSetId, folderNodeRef); + } + + return rules.stream() + .map(this::mapToServiceModelAndValidateActions) + .map(rule -> ruleService.saveRule(folderNodeRef, rule)) + .map(rule -> ruleLoader.loadRule(rule, includes)) + .collect(Collectors.toList()); + } + + @Override + public Rule updateRuleById(String folderNodeId, String ruleSetId, String ruleId, Rule rule, List includes) + { + LOGGER.debug("Updating rule in folder {}, rule set {}, rule {} to {}", folderNodeId, ruleSetId, ruleId, rule); + + NodeRef folderNodeRef = validator.validateFolderNode(folderNodeId, true); + NodeRef ruleSetNodeRef = validator.validateRuleSetNode(ruleSetId, folderNodeRef); + validator.validateRuleNode(ruleId, ruleSetNodeRef); + + return ruleLoader.loadRule(ruleService.saveRule(folderNodeRef, mapToServiceModelAndValidateActions(rule)), includes); + } + + @Override + public void deleteRuleById(String folderNodeId, String ruleSetId, String ruleId) + { + final NodeRef folderNodeRef = validator.validateFolderNode(folderNodeId, true); + final NodeRef ruleSetNodeRef = validator.validateRuleSetNode(ruleSetId, folderNodeRef); + final NodeRef ruleNodeRef = validator.validateRuleNode(ruleId, ruleSetNodeRef); + final org.alfresco.service.cmr.rule.Rule rule = ruleService.getRule(ruleNodeRef); + ruleService.removeRule(folderNodeRef, rule); + } + + @Override + public RuleExecution executeRules(final String folderNodeId, final boolean eachSubFolderIncluded) + { + final NodeRef folderNodeRef = validator.validateFolderNode(folderNodeId, false); + final Map parameterValues = new HashMap<>(); + parameterValues.put(ExecuteAllRulesActionExecuter.PARAM_RUN_ALL_RULES_ON_CHILDREN, eachSubFolderIncluded); + parameterValues.put(ExecuteAllRulesActionExecuter.PARAM_EXECUTE_INHERITED_RULES, true); + final ActionImpl action = new ActionImpl(null, GUID.generate(), ExecuteAllRulesActionExecuter.NAME); + action.setNodeRef(folderNodeRef); + action.setParameterValues(parameterValues); + + ActionAccessRestriction.setActionContext(action, ActionAccessRestriction.V1_ACTION_CONTEXT); + actionService.executeAction(action, folderNodeRef, true, false); + + return RuleExecution.builder() + .eachSubFolderIncluded(eachSubFolderIncluded) + .create(); + } + + private org.alfresco.service.cmr.rule.Rule mapToServiceModelAndValidateActions(Rule rule) + { + if (CollectionUtils.isEmpty(rule.getActions())) + { + throw new InvalidArgumentException(MUST_HAVE_AT_LEAST_ONE_ACTION); + } + final org.alfresco.service.cmr.rule.Rule serviceModelRule = ruleMapper.toServiceModel(rule); + + return actionPermissionValidator.validateRulePermissions(serviceModelRule); + } + + public void setActionService(ActionService actionService) + { + this.actionService = actionService; + } + + public void setRuleService(RuleService ruleService) + { + this.ruleService = ruleService; + } + + public void setValidator(NodeValidator validator) + { + this.validator = validator; + } + + public void setRuleLoader(RuleLoader ruleLoader) + { + this.ruleLoader = ruleLoader; + } + + public void setActionPermissionValidator(ActionPermissionValidator actionPermissionValidator) + { + this.actionPermissionValidator = actionPermissionValidator; + } + + public void setRuleMapper( + RestModelMapper ruleMapper) + { + this.ruleMapper = ruleMapper; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/validator/actions/ActionNodeParameterValidator.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/validator/actions/ActionNodeParameterValidator.java new file mode 100644 index 0000000000..b8c3d8933e --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/validator/actions/ActionNodeParameterValidator.java @@ -0,0 +1,171 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.validator.actions; + +import static org.alfresco.model.ContentModel.TYPE_CATEGORY; +import static org.alfresco.model.ContentModel.TYPE_FOLDER; +import static org.alfresco.service.cmr.dictionary.DataTypeDefinition.NODE_REF; +import static org.alfresco.service.cmr.security.AccessStatus.ALLOWED; +import static org.alfresco.service.cmr.security.PermissionService.WRITE; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.alfresco.repo.action.executer.CheckOutActionExecuter; +import org.alfresco.repo.action.executer.CopyActionExecuter; +import org.alfresco.repo.action.executer.ImageTransformActionExecuter; +import org.alfresco.repo.action.executer.ImporterActionExecuter; +import org.alfresco.repo.action.executer.LinkCategoryActionExecuter; +import org.alfresco.repo.action.executer.MoveActionExecuter; +import org.alfresco.repo.action.executer.SimpleWorkflowActionExecuter; +import org.alfresco.repo.action.executer.TransformActionExecuter; +import org.alfresco.rest.api.Actions; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.actions.ActionValidator; +import org.alfresco.rest.api.model.ActionDefinition; +import org.alfresco.rest.api.model.rules.Action; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.NamespaceService; +import org.apache.commons.collections.MapUtils; +import org.apache.logging.log4j.util.Strings; + +/** + * This class provides logic for validation of permissions for action parameters which reference node. + */ +public class ActionNodeParameterValidator implements ActionValidator +{ + /** + * This list holds action parameter names which require only READ permission on a referenced node + * That means, all other parameters that reference nodes will require WRITE permission + */ + static final Map> REQUIRE_READ_PERMISSION_PARAMS = + Map.of(LinkCategoryActionExecuter.NAME, List.of(LinkCategoryActionExecuter.PARAM_CATEGORY_VALUE)); + + static final String NO_PROPER_PERMISSIONS_FOR_NODE = "No proper permissions for node: "; + static final String NOT_A_CATEGORY = "Node is not a category "; + static final String NOT_A_FOLDER = "Node is not a folder "; + + private final Actions actions; + private final NamespaceService namespaceService; + private final Nodes nodes; + private final PermissionService permissionService; + + public ActionNodeParameterValidator(Actions actions, NamespaceService namespaceService, Nodes nodes, + PermissionService permissionService) + { + this.actions = actions; + this.namespaceService = namespaceService; + this.nodes = nodes; + this.permissionService = permissionService; + } + + /** + * Validates action parameters that reference nodes against access permissions for executing user. + * + * @param action Action to be validated + */ + @Override + public void validate(Action action) + { + final ActionDefinition actionDefinition = actions.getRuleActionDefinitionById(action.getActionDefinitionId()); + final List nodeRefParams = actionDefinition.getParameterDefinitions().stream() + .filter(pd -> NODE_REF.toPrefixString(namespaceService).equals(pd.getType())) + .collect(Collectors.toList()); + validateNodes(nodeRefParams, action); + } + + /** + * @return List of action definitions applicable to this validator + */ + @Override + public List getActionDefinitionIds() + { + return List.of(CopyActionExecuter.NAME, MoveActionExecuter.NAME, CheckOutActionExecuter.NAME, ImporterActionExecuter.NAME, + LinkCategoryActionExecuter.NAME, SimpleWorkflowActionExecuter.NAME, TransformActionExecuter.NAME, + ImageTransformActionExecuter.NAME); + } + + @Override + public int getPriority() + { + return Integer.MIN_VALUE + 1; + } + + private void validateNodes(final List nodeRefParamDefinitions, + final Action action) + { + if (MapUtils.isNotEmpty(action.getParams())) + { + nodeRefParamDefinitions.stream() + .filter(pd -> action.getParams().containsKey(pd.getName())) + .forEach(p -> { + final String nodeId = Objects.toString(action.getParams().get(p.getName()), Strings.EMPTY); + final NodeRef nodeRef = nodes.validateNode(nodeId); + validatePermission(action.getActionDefinitionId(), p.getName(), nodeRef); + validateType(action.getActionDefinitionId(), nodeRef); + }); + } + } + + private void validatePermission(final String actionDefinitionId, final String paramName, final NodeRef nodeRef) + { + if (permissionService.hasReadPermission(nodeRef) != ALLOWED) + { + throw new EntityNotFoundException(nodeRef.getId()); + } + if (!REQUIRE_READ_PERMISSION_PARAMS.containsKey(actionDefinitionId) || + REQUIRE_READ_PERMISSION_PARAMS.get(actionDefinitionId).stream().noneMatch(paramName::equals)) + { + if (permissionService.hasPermission(nodeRef, WRITE) != ALLOWED) + { + throw new PermissionDeniedException(NO_PROPER_PERMISSIONS_FOR_NODE + nodeRef.getId()); + } + } + } + + private void validateType(final String actionDefinitionId, final NodeRef nodeRef) + { + if (!LinkCategoryActionExecuter.NAME.equals(actionDefinitionId)) + { + if (!nodes.nodeMatches(nodeRef, Set.of(TYPE_FOLDER), Collections.emptySet())) + { + throw new InvalidArgumentException(NOT_A_FOLDER + nodeRef.getId()); + } + } else if (!nodes.nodeMatches(nodeRef, Set.of(TYPE_CATEGORY), Collections.emptySet())) + { + throw new InvalidArgumentException(NOT_A_CATEGORY + nodeRef.getId()); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/validator/actions/ActionParameterDefinitionValidator.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/validator/actions/ActionParameterDefinitionValidator.java new file mode 100644 index 0000000000..63de95ddc6 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/validator/actions/ActionParameterDefinitionValidator.java @@ -0,0 +1,166 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.validator.actions; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.alfresco.rest.api.Actions; +import org.alfresco.rest.api.actions.ActionValidator; +import org.alfresco.rest.api.model.ActionDefinition; +import org.alfresco.rest.api.model.ActionParameterConstraint; +import org.alfresco.rest.api.model.rules.Action; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.service.Experimental; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.logging.log4j.util.Strings; + +/** + * This class will validate all action types against action parameters definitions (mandatory parameters, parameter constraints) + */ +@Experimental +public class ActionParameterDefinitionValidator implements ActionValidator +{ + static final String INVALID_PARAMETER_VALUE = + "Action parameter: %s has invalid value (%s). Look up possible values for constraint name %s"; + static final String MISSING_PARAMETER = "Missing action's mandatory parameter: %s"; + static final String MUST_NOT_CONTAIN_PARAMETER = "Action of definition id: %s must not contain parameter of name: %s"; + static final String PARAMS_SHOULD_NOT_BE_EMPTY = + "Action parameters should not be null or empty for this action. See Action Definition for action of: %s"; + static final String INVALID_ACTION_DEFINITION = "Invalid rule action definition requested %s"; + static final String EMPTY_ACTION_DEFINITION = "Empty/null rule action definition id"; + + private final Actions actions; + + public ActionParameterDefinitionValidator(Actions actions) + { + this.actions = actions; + } + + /** + * Validates action against its parameters definitions (mandatory parameters, parameter constraints) + * + * @param action Action to be validated + */ + @Override + public void validate(Action action) + { + ActionDefinition actionDefinition; + final String actionDefinitionId = action.getActionDefinitionId(); + if (Strings.isBlank(actionDefinitionId)) + { + throw new InvalidArgumentException(EMPTY_ACTION_DEFINITION); + } + try + { + actionDefinition = actions.getRuleActionDefinitionById(actionDefinitionId); + } catch (NotFoundException e) + { + throw new InvalidArgumentException(String.format(INVALID_ACTION_DEFINITION, actionDefinitionId)); + } + validateParametersSize(action.getParams(), actionDefinition); + final Map params = action.getParams(); + if (MapUtils.isNotEmpty(params)) + { + params.forEach((key, value) -> checkParameterShouldExist(key, actionDefinition)); + getParameterDefinitions(actionDefinition).forEach(p -> validateParameterDefinitions(p, params)); + } + } + + /** + * This validator should be applied to all actions + * + * @return list of all defined action definition ids + */ + @Override + public List getActionDefinitionIds() + { + return List.of(ALL_ACTIONS); + } + + /** + * This validator should have highest priority and be executed first of all (thus minimal integer is returned here). + * + * @return minimal integer value + */ + @Override + public int getPriority() + { + return Integer.MIN_VALUE; + } + + private void validateParametersSize(final Map params, final ActionDefinition actionDefinition) + { + final List parameterDefinitions = getParameterDefinitions(actionDefinition); + if (CollectionUtils.isNotEmpty( + parameterDefinitions.stream().filter(ActionDefinition.ParameterDefinition::isMandatory).collect(Collectors.toList())) && + MapUtils.isEmpty(params)) + { + throw new InvalidArgumentException(String.format(PARAMS_SHOULD_NOT_BE_EMPTY, actionDefinition.getName())); + } + } + + private List getParameterDefinitions(ActionDefinition actionDefinition) + { + return actionDefinition.getParameterDefinitions() == null ? Collections.emptyList() : actionDefinition.getParameterDefinitions(); + } + + private void validateParameterDefinitions(final ActionDefinition.ParameterDefinition parameterDefinition, + final Map params) + { + final Serializable parameterValue = params.get(parameterDefinition.getName()); + if (parameterDefinition.isMandatory() && parameterValue == null) + { + throw new InvalidArgumentException(String.format(MISSING_PARAMETER, parameterDefinition.getName())); + } + if (parameterDefinition.getParameterConstraintName() != null) + { + final ActionParameterConstraint actionConstraint = + actions.getActionConstraint(parameterDefinition.getParameterConstraintName()); + if (parameterValue != null && actionConstraint.getConstraintValues().stream() + .noneMatch(constraintData -> constraintData.getValue().equals(Objects.toString(parameterValue, null)))) + { + throw new InvalidArgumentException(String.format(INVALID_PARAMETER_VALUE, parameterDefinition.getName(), parameterValue, + actionConstraint.getConstraintName())); + } + } + } + + private void checkParameterShouldExist(final String parameterName, final ActionDefinition actionDefinition) + { + if (getParameterDefinitions(actionDefinition).stream().noneMatch(pd -> parameterName.equals(pd.getName()))) + { + throw new InvalidArgumentException(String.format(MUST_NOT_CONTAIN_PARAMETER, actionDefinition.getName(), parameterName)); + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/ActionParameterConstraint.java b/remote-api/src/main/java/org/alfresco/rest/api/model/ActionParameterConstraint.java new file mode 100644 index 0000000000..c20815e422 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/ActionParameterConstraint.java @@ -0,0 +1,99 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2021 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.model; + +import java.util.List; + +import org.alfresco.service.Experimental; + +/** + * Representation of action parameter constraint. + * Helps to constraint the list of allowable values for an action parameter. + * When action parameter has constraints defined (@see ActionDefinition.ParameterDefinition#getParameterConstraintName()) + * they will be listed here. + * + * @author mpichura + */ +@Experimental +public class ActionParameterConstraint +{ + /** + * Constraint name. + */ + private String constraintName; + /** + * List of objects representing constraint values along with additional data + */ + private List constraintValues; + + public List getConstraintValues() + { + return constraintValues; + } + + public void setConstraintValues(List constraintValues) + { + this.constraintValues = constraintValues; + } + + public String getConstraintName() + { + return constraintName; + } + + public void setConstraintName(String constraintName) + { + this.constraintName = constraintName; + } + + public static class ConstraintData + { + public ConstraintData(final String value, final String label) + { + this.value = value; + this.label = label; + } + /** + * Actual constraint value + */ + private String value; + /** + * A label associated to constraint's value + */ + private String label; + + public String getValue() + { + return value; + } + + public String getLabel() + { + return label; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/RepositoryInfo.java b/remote-api/src/main/java/org/alfresco/rest/api/model/RepositoryInfo.java index 60bbadd32a..b177942b84 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/model/RepositoryInfo.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/RepositoryInfo.java @@ -284,7 +284,8 @@ public class RepositoryInfo .setMaxDocs(licenseDescriptor.getMaxDocs()) .setMaxUsers(licenseDescriptor.getMaxUsers()) .setClusterEnabled(licenseDescriptor.isClusterEnabled()) - .setCryptodocEnabled(licenseDescriptor.isCryptodocEnabled()); + .setCryptodocEnabled(licenseDescriptor.isCryptodocEnabled()) + .setCustomEmbeddedWorkflowEnabled(licenseDescriptor.isCustomEmbeddedWorkflowEnabled()); } public Date getIssuedAt() @@ -343,6 +344,7 @@ public class RepositoryInfo private Long maxDocs; private boolean isClusterEnabled; private boolean isCryptodocEnabled; + private boolean isCustomEmbeddedWorkflowEnabled; public LicenseEntitlement() { @@ -392,6 +394,17 @@ public class RepositoryInfo return this; } + public boolean getIsCustomEmbeddedWorkflowEnabled() + { + return isCustomEmbeddedWorkflowEnabled; + } + + public LicenseEntitlement setCustomEmbeddedWorkflowEnabled(boolean customEmbeddedWorkflowEnabled) + { + isCustomEmbeddedWorkflowEnabled = customEmbeddedWorkflowEnabled; + return this; + } + @Override public String toString() { @@ -400,6 +413,7 @@ public class RepositoryInfo .append(", maxDocs=").append(maxDocs) .append(", isClusterEnabled=").append(isClusterEnabled) .append(", isCryptodocEnabled=").append(isCryptodocEnabled) + .append(", isCustomEmbeddedWorkflowEnabled=").append(isCustomEmbeddedWorkflowEnabled) .append(']'); return sb.toString(); } diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/mapper/RestModelMapper.java b/remote-api/src/main/java/org/alfresco/rest/api/model/mapper/RestModelMapper.java new file mode 100644 index 0000000000..539a328bd4 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/mapper/RestModelMapper.java @@ -0,0 +1,70 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.model.mapper; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.alfresco.service.Experimental; +import org.apache.commons.lang3.NotImplementedException; + +@Experimental +public interface RestModelMapper +{ + default R toRestModel(S serviceModel) { + throw new NotImplementedException(); + } + default S toServiceModel(R restModel) { + throw new NotImplementedException(); + } + default R toRestModel(Collection serviceModels) { + throw new NotImplementedException(); + } + default S toServiceModel(Collection restModels) { + throw new NotImplementedException(); + } + default List toRestModels(Collection serviceModels) { + return serviceModels.stream() + .filter(Objects::nonNull) + .map(this::toRestModel) + .collect(Collectors.toList()); + } + default List toServiceModels(Collection restModels) { + return restModels.stream() + .filter(Objects::nonNull) + .map(this::toServiceModel) + .collect(Collectors.toList()); + } + default List toRestModels(S serviceModel) { + throw new NotImplementedException(); + } + default List toServiceModels(R restModel) { + throw new NotImplementedException(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/rules/Action.java b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/Action.java new file mode 100644 index 0000000000..c3befba7c2 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/Action.java @@ -0,0 +1,113 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.model.rules; + +import java.io.Serializable; +import java.util.Map; +import java.util.Objects; + +import org.alfresco.service.Experimental; + +@Experimental +public class Action +{ + private String actionDefinitionId; + private Map params; + + public String getActionDefinitionId() + { + return actionDefinitionId; + } + + public void setActionDefinitionId(String actionDefinitionId) + { + this.actionDefinitionId = actionDefinitionId; + } + + public Map getParams() + { + return params; + } + + public void setParams(Map params) + { + this.params = params; + } + + @Override + public String toString() + { + return "Action{" + "actionDefinitionId='" + actionDefinitionId + '\'' + ", params=" + params + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Action action = (Action) o; + return Objects.equals(actionDefinitionId, action.actionDefinitionId) && Objects.equals(params, action.params); + } + + @Override + public int hashCode() + { + return Objects.hash(actionDefinitionId, params); + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private String actionDefinitionId; + private Map params; + + public Builder actionDefinitionId(String actionDefinitionId) + { + this.actionDefinitionId = actionDefinitionId; + return this; + } + + public Builder params(Map params) + { + this.params = params; + return this; + } + + public Action create() { + final Action action = new Action(); + action.setActionDefinitionId(actionDefinitionId); + action.setParams(params); + return action; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/rules/CompositeCondition.java b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/CompositeCondition.java new file mode 100644 index 0000000000..b773fba7b8 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/CompositeCondition.java @@ -0,0 +1,165 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.model.rules; + +import java.util.List; +import java.util.Objects; + +import org.alfresco.service.Experimental; + +@Experimental +public class CompositeCondition +{ + private boolean inverted; + private ConditionOperator booleanMode = ConditionOperator.AND; + private List compositeConditions; + private List simpleConditions; + + public boolean isInverted() + { + return inverted; + } + + public void setInverted(boolean inverted) + { + this.inverted = inverted; + } + + public String getBooleanMode() + { + if (booleanMode == null) + { + return null; + } + return booleanMode.name().toLowerCase(); + } + + public void setBooleanMode(String booleanMode) + { + if (booleanMode != null) + { + this.booleanMode = ConditionOperator.valueOf(booleanMode.toUpperCase()); + } + } + + public void setBooleanMode(ConditionOperator booleanMode) + { + this.booleanMode = booleanMode; + } + + public List getCompositeConditions() + { + return compositeConditions; + } + + public void setCompositeConditions(List compositeConditions) + { + this.compositeConditions = compositeConditions; + } + + public List getSimpleConditions() + { + return simpleConditions; + } + + public void setSimpleConditions(List simpleConditions) + { + this.simpleConditions = simpleConditions; + } + + @Override + public String toString() + { + return "CompositeCondition{" + "inverted=" + inverted + ", booleanMode=" + booleanMode + ", compositeConditions=" + compositeConditions + ", simpleConditions=" + + simpleConditions + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + CompositeCondition that = (CompositeCondition) o; + return inverted == that.inverted && booleanMode == that.booleanMode && Objects.equals(compositeConditions, that.compositeConditions) && Objects.equals( + simpleConditions, that.simpleConditions); + } + + @Override + public int hashCode() + { + return Objects.hash(inverted, booleanMode, compositeConditions, simpleConditions); + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private boolean inverted; + private ConditionOperator booleanMode = ConditionOperator.AND; + private List compositeConditions; + private List simpleConditions; + + public Builder inverted(boolean inverted) + { + this.inverted = inverted; + return this; + } + + public Builder booleanMode(ConditionOperator booleanMode) + { + this.booleanMode = booleanMode; + return this; + } + + public Builder compositeConditions(List compositeConditions) + { + this.compositeConditions = compositeConditions; + return this; + } + + public Builder simpleConditions(List simpleConditions) + { + this.simpleConditions = simpleConditions; + return this; + } + + public CompositeCondition create() + { + final CompositeCondition condition = new CompositeCondition(); + condition.setInverted(inverted); + condition.setBooleanMode(booleanMode); + condition.setCompositeConditions(compositeConditions); + condition.setSimpleConditions(simpleConditions); + return condition; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRuleSetsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/ConditionOperator.java similarity index 78% rename from remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRuleSetsRelation.java rename to remote-api/src/main/java/org/alfresco/rest/api/model/rules/ConditionOperator.java index 4a3d3163f9..a61d288339 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRuleSetsRelation.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/ConditionOperator.java @@ -24,17 +24,12 @@ * #L% */ -package org.alfresco.rest.api.nodes; +package org.alfresco.rest.api.model.rules; -import org.alfresco.rest.framework.resource.RelationshipResource; import org.alfresco.service.Experimental; -/** - * Folder node rule sets. - * - */ @Experimental -@RelationshipResource(name = "rule-sets", entityResource = NodesEntityResource.class, title = "Folder node rule sets") -public class NodeRuleSetsRelation +public enum ConditionOperator { + AND, OR } diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/rules/InclusionType.java b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/InclusionType.java new file mode 100644 index 0000000000..687cde0f4d --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/InclusionType.java @@ -0,0 +1,59 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.model.rules; + +import com.fasterxml.jackson.annotation.JsonValue; + +import org.alfresco.service.Experimental; + +/** The reason why a rule set applies to a folder. */ +@Experimental +public enum InclusionType +{ + OWNED, INHERITED, LINKED; + + /** + * Load an InclusionType from a given string (case insensitively). + * + * @param inclusionType The given string. + * @return The inclusion type. + */ + public static InclusionType from(String inclusionType) + { + return InclusionType.valueOf(inclusionType.toUpperCase()); + } + + /** + * The lower case version of the inclusion type. + * + * @return A lowercase string. + */ + @JsonValue + public String toString() + { + return super.toString().toLowerCase(); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/rules/Rule.java b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/Rule.java index d44291abd5..aa29eaef95 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/model/rules/Rule.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/Rule.java @@ -26,31 +26,27 @@ package org.alfresco.rest.api.model.rules; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + import org.alfresco.rest.framework.resource.UniqueId; import org.alfresco.service.Experimental; -import org.alfresco.service.cmr.action.CompositeAction; - -import java.util.List; -import java.util.stream.Collectors; @Experimental public class Rule { - private String id; private String name; - - public static Rule from(final org.alfresco.service.cmr.rule.Rule ruleModel) { - if (ruleModel == null) { - return null; - } - - final Rule rule = new Rule(); - rule.id = ruleModel.getNodeRef().getId(); - rule.name = ruleModel.getTitle(); - - return rule; - } + private String description; + private boolean isEnabled; + private boolean isInheritable; + private boolean isAsynchronous; + private Boolean isShared; + private String errorScript; + private List triggers = List.of(RuleTrigger.INBOUND); + private CompositeCondition conditions; + private List actions; @UniqueId public String getId() @@ -73,9 +69,244 @@ public class Rule this.name = name; } + public String getDescription() + { + return description; + } + + public void setDescription(String description) + { + this.description = description; + } + + public boolean getIsEnabled() + { + return isEnabled; + } + + public void setIsEnabled(boolean isEnabled) + { + this.isEnabled = isEnabled; + } + + public boolean getIsInheritable() + { + return isInheritable; + } + + public void setIsInheritable(boolean isInheritable) + { + this.isInheritable = isInheritable; + } + + public boolean getIsAsynchronous() + { + return isAsynchronous; + } + + public void setIsAsynchronous(boolean isAsynchronous) + { + this.isAsynchronous = isAsynchronous; + } + + public String getErrorScript() + { + return errorScript; + } + + public void setErrorScript(String errorScript) + { + this.errorScript = errorScript; + } + + public Boolean isIsShared() + { + return isShared; + } + + public void setIsShared(Boolean shared) + { + this.isShared = shared; + } + + public List getTriggers() + { + if (triggers == null) + { + return null; + } + return triggers.stream().map(RuleTrigger::getValue).collect(Collectors.toList()); + } + + public void setTriggers(List triggers) + { + if (triggers != null) + { + this.triggers = triggers.stream().map(RuleTrigger::of).collect(Collectors.toList()); + } + } + + public void setRuleTriggers(List triggers) + { + this.triggers = triggers; + } + + public CompositeCondition getConditions() + { + return conditions; + } + + public void setConditions(CompositeCondition conditions) + { + this.conditions = conditions; + } + + public List getActions() + { + return actions; + } + + public void setActions(List actions) + { + this.actions = actions; + } + @Override public String toString() { - return "Rule{" + "id='" + id + '\'' + ", name='" + name + '\'' + '}'; + return "Rule{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", description='" + description + '\'' + ", isEnabled=" + isEnabled + ", isInheritable=" + isInheritable + + ", isAsynchronous=" + isAsynchronous + ", isShared=" + isShared + ", errorScript='" + errorScript + '\'' + ", triggers=" + triggers + ", conditions=" + conditions + + ", actions=" + actions + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Rule rule = (Rule) o; + return isEnabled == rule.isEnabled + && isInheritable == rule.isInheritable + && isAsynchronous == rule.isAsynchronous + && Objects.equals(isShared, rule.isShared) + && Objects.equals(id, rule.id) + && Objects.equals(name, rule.name) + && Objects.equals(description, rule.description) + && Objects.equals(errorScript, rule.errorScript) + && Objects.equals(triggers, rule.triggers) + && Objects.equals(conditions, rule.conditions) + && Objects.equals(actions, rule.actions); + } + + @Override + public int hashCode() + { + return Objects.hash(id, name, description, isEnabled, isInheritable, isAsynchronous, isShared, errorScript, triggers, conditions, actions); + } + + public static Builder builder() + { + return new Builder(); + } + + /** Builder class. */ + public static class Builder + { + private String id; + private String name; + private String description; + private boolean isEnabled; + private boolean isInheritable; + private boolean isAsynchronous; + private Boolean isShared; + private String errorScript; + private List triggers = List.of(RuleTrigger.INBOUND); + private CompositeCondition conditions; + private List actions; + + public Builder id(String id) + { + this.id = id; + return this; + } + + public Builder name(String name) + { + this.name = name; + return this; + } + + public Builder description(String description) + { + this.description = description; + return this; + } + + public Builder isEnabled(boolean isEnabled) + { + this.isEnabled = isEnabled; + return this; + } + + public Builder isInheritable(boolean isInheritable) + { + this.isInheritable = isInheritable; + return this; + } + + public Builder isAsynchronous(boolean isAsynchronous) + { + this.isAsynchronous = isAsynchronous; + return this; + } + + public Builder isShared(Boolean isShared) + { + this.isShared = isShared; + return this; + } + + public Builder errorScript(String errorScript) + { + this.errorScript = errorScript; + return this; + } + + public Builder triggers(List triggers) + { + this.triggers = triggers; + return this; + } + + public Builder conditions(CompositeCondition conditions) + { + this.conditions = conditions; + return this; + } + + public Builder actions(List actions) + { + this.actions = actions; + return this; + } + + public Rule create() + { + Rule rule = new Rule(); + rule.setId(id); + rule.setName(name); + rule.setDescription(description); + rule.setIsEnabled(isEnabled); + rule.setIsInheritable(isInheritable); + rule.setIsAsynchronous(isAsynchronous); + rule.setIsShared(isShared); + rule.setErrorScript(errorScript); + rule.setRuleTriggers(triggers); + rule.setConditions(conditions); + rule.setActions(actions); + return rule; + } } } diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleExecution.java b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleExecution.java new file mode 100644 index 0000000000..35029f42c5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleExecution.java @@ -0,0 +1,92 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.model.rules; + +import java.util.Objects; + +import org.alfresco.service.Experimental; + +@Experimental +public class RuleExecution +{ + private boolean eachSubFolderIncluded; + + public boolean getIsEachSubFolderIncluded() + { + return eachSubFolderIncluded; + } + + public void setIsEachSubFolderIncluded(boolean eachSubFolderIncluded) + { + this.eachSubFolderIncluded = eachSubFolderIncluded; + } + + @Override + public String toString() + { + return "RuleExecution{" + "eachSubFolderIncluded=" + eachSubFolderIncluded + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + RuleExecution that = (RuleExecution) o; + return eachSubFolderIncluded == that.eachSubFolderIncluded; + } + + @Override + public int hashCode() + { + return Objects.hash(eachSubFolderIncluded); + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private boolean eachSubFolderIncluded; + + public Builder eachSubFolderIncluded(boolean eachSubFolderIncluded) + { + this.eachSubFolderIncluded = eachSubFolderIncluded; + return this; + } + + public RuleExecution create() + { + final RuleExecution ruleExecution = new RuleExecution(); + ruleExecution.setIsEachSubFolderIncluded(eachSubFolderIncluded); + return ruleExecution; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleSet.java b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleSet.java index 5202dfbbcd..3e7fa5f794 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleSet.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleSet.java @@ -26,29 +26,32 @@ package org.alfresco.rest.api.model.rules; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; + import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.repository.NodeRef; @Experimental public class RuleSet { - private static final String DEFAULT_ID = "-default-"; + public static final String DEFAULT_ID = "-default-"; private String id; + private NodeRef owningFolder; + private InclusionType inclusionType; + private List inheritedBy; + private List linkedToBy; + private Boolean isInherited; + private Boolean isLinkedTo; + private List ruleIds; public static RuleSet of(String id) { - final RuleSet ruleSet = new RuleSet(); - ruleSet.id = id; - - return ruleSet; - } - - public boolean isNotDefaultId() { - return isNotDefaultId(this.id); - } - - public boolean isDefaultId() { - return isDefaultId(this.id); + return builder() + .id(id) + .create(); } public static boolean isNotDefaultId(final String id) { @@ -69,9 +72,213 @@ public class RuleSet this.id = id; } + public NodeRef getOwningFolder() + { + return owningFolder; + } + + public void setOwningFolder(NodeRef owningFolder) + { + this.owningFolder = owningFolder; + } + + public InclusionType getInclusionType() + { + return inclusionType; + } + + public void setInclusionType(InclusionType inclusionType) + { + this.inclusionType = inclusionType; + } + + public List getInheritedBy() + { + return inheritedBy; + } + + public void setInheritedBy(List inheritedBy) + { + this.inheritedBy = inheritedBy; + } + + public List getLinkedToBy() + { + return linkedToBy; + } + + public void setLinkedToBy(List linkedToBy) + { + this.linkedToBy = linkedToBy; + } + + /** + * Set a flag indicating that the rule set is inherited by a folder. + * + * @param inherited The flag. + */ + public void setIsInherited(Boolean inherited) + { + isInherited = inherited; + } + + /** + * Find if the rule set is inherited by a folder. + * + * @return The value of the flag. + */ + public Boolean getIsInherited() + { + return isInherited; + } + + /** + * Set a flag indicating that the rule set is linked to by a folder. + * + * @param isLinkedTo The flag. + */ + public void setIsLinkedTo(Boolean isLinkedTo) + { + this.isLinkedTo = isLinkedTo; + } + + /** + * Find if the rule set is linked to by a folder. + * + * @return The value of the flag. + */ + public Boolean getIsLinkedTo() + { + return isLinkedTo; + } + + public List getRuleIds() + { + return ruleIds; + } + + public void setRuleIds(List ruleIds) + { + this.ruleIds = ruleIds; + } + @Override public String toString() { - return "RuleSet{" + "id='" + id + '\'' + '}'; + return "RuleSet{" + + new StringJoiner(", ") + .add("id='" + id + "'") + .add("owningFolder='" + owningFolder + "'") + .add("inclusionType='" + inclusionType + "'") + .add("inheritedBy='" + inheritedBy + "'") + .add("linkedToBy='" + linkedToBy + "'") + .add("isInherited='" + isInherited + "'") + .add("isLinkedTo='" + isLinkedTo + "'") + .add("ruleIds='" + ruleIds + "'") + .toString() + + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + RuleSet ruleSet = (RuleSet) o; + return Objects.equals(id, ruleSet.id) + && Objects.equals(owningFolder, ruleSet.owningFolder) + && inclusionType == ruleSet.inclusionType + && Objects.equals(inheritedBy, ruleSet.inheritedBy) + && Objects.equals(linkedToBy, ruleSet.linkedToBy) + && Objects.equals(isInherited, ruleSet.isInherited) + && Objects.equals(isLinkedTo, ruleSet.isLinkedTo) + && Objects.equals(ruleIds, ruleSet.ruleIds); + } + + @Override + public int hashCode() + { + return Objects.hash(id, owningFolder, inclusionType, inheritedBy, linkedToBy, isInherited, isLinkedTo, ruleIds); + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private String id; + private NodeRef owningFolder; + private InclusionType inclusionType; + private List inheritedBy; + private List linkedToBy; + private Boolean isInherited; + private Boolean isLinkedTo; + private List ruleIds; + + public Builder id(String id) + { + this.id = id; + return this; + } + + public Builder owningFolder(NodeRef owningFolder) + { + this.owningFolder = owningFolder; + return this; + } + + public Builder inclusionType(InclusionType inclusionType) + { + this.inclusionType = inclusionType; + return this; + } + + public Builder inheritedBy(List inheritedBy) + { + this.inheritedBy = inheritedBy; + return this; + } + + public Builder linkedToBy(List linkedToBy) + { + this.linkedToBy = linkedToBy; + return this; + } + + public Builder isInherited(Boolean isInherited) + { + this.isInherited = isInherited; + return this; + } + + public Builder isLinkedTo(Boolean isLinkedTo) + { + this.isLinkedTo = isLinkedTo; + return this; + } + + public Builder ruleIds(List ruleIds) + { + this.ruleIds = ruleIds; + return this; + } + + public RuleSet create() + { + final RuleSet ruleSet = new RuleSet(); + ruleSet.setId(id); + ruleSet.setOwningFolder(owningFolder); + ruleSet.setInclusionType(inclusionType); + ruleSet.setInheritedBy(inheritedBy); + ruleSet.setLinkedToBy(linkedToBy); + ruleSet.setIsInherited(isInherited); + ruleSet.setIsLinkedTo(isLinkedTo); + ruleSet.setRuleIds(ruleIds); + return ruleSet; + } } } diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleSetLink.java b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleSetLink.java new file mode 100644 index 0000000000..56eb6b12af --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleSetLink.java @@ -0,0 +1,46 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.model.rules; + +public class RuleSetLink +{ + + /** + * This id is referring to the node id of either the rule set or the folder that contains the rule set. + */ + private String id; + + public void setId(String id) + { + this.id = id; + } + + public String getId() + { + return id; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleSetting.java b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleSetting.java new file mode 100644 index 0000000000..892a1f75d5 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleSetting.java @@ -0,0 +1,126 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.model.rules; + +import java.util.Objects; +import java.util.StringJoiner; + +import org.alfresco.rest.framework.resource.UniqueId; +import org.alfresco.service.Experimental; + +@Experimental +public class RuleSetting +{ + public static final String IS_INHERITANCE_ENABLED_KEY = "-isInheritanceEnabled-"; + + private String key; + private Object value; + + @UniqueId + public String getKey() + { + return key; + } + + public void setKey(String key) + { + this.key = key; + } + + public Object getValue() + { + return value; + } + + public void setValue(Object value) + { + this.value = value; + } + + @Override + public String toString() + { + return "RuleSetting{" + + new StringJoiner(", ") + .add("key=" + key) + .add("value=" + value.toString()) + .toString() + + "}"; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (!(o instanceof RuleSetting)) + { + return false; + } + RuleSetting that = (RuleSetting) o; + return Objects.equals(key, that.key) + && Objects.equals(value, that.value); + } + + @Override + public int hashCode() + { + return Objects.hash(key, value); + } + + public static RuleSetting.Builder builder() + { + return new RuleSetting.Builder(); + } + + public static class Builder + { + private String key; + private Object value; + + public RuleSetting.Builder key(String key) + { + this.key = key; + return this; + } + + public RuleSetting.Builder value(Object value) + { + this.value = value; + return this; + } + + public RuleSetting create() + { + final RuleSetting ruleSetting = new RuleSetting(); + ruleSetting.setKey(key); + ruleSetting.setValue(value); + return ruleSetting; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleTrigger.java b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleTrigger.java new file mode 100644 index 0000000000..a2b370329c --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/RuleTrigger.java @@ -0,0 +1,45 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.model.rules; + +import org.alfresco.service.Experimental; + +@Experimental +public enum RuleTrigger +{ + INBOUND, UPDATE, OUTBOUND; + + public String getValue() + { + return this.name().toLowerCase(); + } + + public static RuleTrigger of(final String value) + { + return RuleTrigger.valueOf(value.toUpperCase()); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/model/rules/SimpleCondition.java b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/SimpleCondition.java new file mode 100644 index 0000000000..b1a33eaa6f --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/model/rules/SimpleCondition.java @@ -0,0 +1,156 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.model.rules; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ActionConditionImpl; +import org.alfresco.repo.action.evaluator.CompareMimeTypeEvaluator; +import org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator; +import org.alfresco.repo.action.evaluator.HasAspectEvaluator; +import org.alfresco.repo.action.evaluator.HasTagEvaluator; +import org.alfresco.repo.action.evaluator.InCategoryEvaluator; +import org.alfresco.repo.action.evaluator.IsSubTypeEvaluator; +import org.alfresco.repo.action.evaluator.NoConditionEvaluator; +import org.alfresco.repo.action.evaluator.compare.ComparePropertyValueOperation; +import org.alfresco.repo.action.evaluator.compare.ContentPropertyName; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.mapper.RestModelMapper; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.collections.CollectionUtils; + +@Experimental +public class SimpleCondition +{ + private String field; + private String comparator; + private String parameter; + + public String getField() + { + return field; + } + + public void setField(String field) + { + this.field = field; + } + + public String getComparator() + { + return comparator; + } + + public void setComparator(String comparator) + { + this.comparator = comparator; + } + + public String getParameter() + { + return parameter; + } + + public void setParameter(String parameter) + { + this.parameter = parameter; + } + + @Override + public String toString() + { + return "SimpleCondition{" + "field='" + field + '\'' + ", comparator='" + comparator + '\'' + ", parameter='" + parameter + '\'' + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + SimpleCondition that = (SimpleCondition) o; + return Objects.equals(field, that.field) && Objects.equals(comparator, that.comparator) && Objects.equals(parameter, that.parameter); + } + + @Override + public int hashCode() + { + return Objects.hash(field, comparator, parameter); + } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private String field; + private String comparator; + private String parameter; + + public Builder field(String field) + { + this.field = field; + return this; + } + + public Builder comparator(String comparator) + { + this.comparator = comparator; + return this; + } + + public Builder parameter(String parameter) + { + this.parameter = parameter; + return this; + } + + public SimpleCondition create() { + final SimpleCondition condition = new SimpleCondition(); + condition.setField(field); + condition.setComparator(comparator); + condition.setParameter(parameter); + return condition; + } + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/rules/NodeRuleExecutionsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/rules/NodeRuleExecutionsRelation.java new file mode 100644 index 0000000000..e82991ff88 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/rules/NodeRuleExecutionsRelation.java @@ -0,0 +1,71 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.rules; + +import java.util.List; + +import org.alfresco.rest.api.Rules; +import org.alfresco.rest.api.model.rules.RuleExecution; +import org.alfresco.rest.api.nodes.NodesEntityResource; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.Experimental; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; + +@Experimental +@RelationshipResource(name = "rule-executions", entityResource = NodesEntityResource.class, title = "Executing rules") +public class NodeRuleExecutionsRelation implements RelationshipResourceAction.Create, InitializingBean +{ + private final Rules rules; + + public NodeRuleExecutionsRelation(Rules rules) + { + this.rules = rules; + } + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "rules", this.rules); + } + + /** + * Execute rules for given folder node. + * + * @param folderNodeId - the ID of a folder + * @param ruleExecutionParameters - rule execution parameters + * @param parameters - additional request parameters + * @return execution details + */ + @Override + public List create(String folderNodeId, List ruleExecutionParameters, Parameters parameters) + { + final RuleExecution ruleExecution = ruleExecutionParameters.stream().findFirst().orElse(new RuleExecution()); + return List.of(rules.executeRules(folderNodeId, ruleExecution.getIsEachSubFolderIncluded())); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/rules/NodeRuleSetLinksRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/rules/NodeRuleSetLinksRelation.java new file mode 100644 index 0000000000..9dbc087125 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/rules/NodeRuleSetLinksRelation.java @@ -0,0 +1,95 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.rules; + +import java.util.List; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.rest.api.RuleSets; +import org.alfresco.rest.api.model.rules.RuleSetLink; +import org.alfresco.rest.api.nodes.NodesEntityResource; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.WebApiParam; +import org.alfresco.rest.framework.core.ResourceParameter; +import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundException; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; + + +@RelationshipResource(name = "rule-set-links", entityResource = NodesEntityResource.class, title = "Rule set links") +public class NodeRuleSetLinksRelation implements InitializingBean, RelationshipResourceAction.Create, + RelationshipResourceAction.Delete +{ + + private final RuleSets ruleSets; + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "ruleSets", ruleSets); + } + + @WebApiParam(name = "ruleSetLinkRequest", title = "Request body - rule set id", + description = "Request body with rule set id", kind = ResourceParameter.KIND.HTTP_BODY_OBJECT) + @WebApiDescription(title = "Link a rule set to a folder node", + description = "Submits a request to link a rule set to folder", + successStatus = HttpServletResponse.SC_CREATED) + @Override + public List create(String nodeId, List ruleSetLinksBody, Parameters parameters) + { + return ruleSetLinksBody.stream() + .map(r -> ruleSets.linkToRuleSet(nodeId, r.getId())) + .collect(Collectors.toList()); + } + + /** + * Remove link between a rule set and a folder for given rule set's and folder's node IDs. + *

+ * - DELETE /nodes/{folderNodeId}/rule-set-links/{ruleSetId} + * + * @param folderNodeId - folder node ID + * @param ruleSetNodeId - rule set node ID (associated with folder node) + * @throws RelationshipResourceNotFoundException in case resource was not found + */ + @WebApiDescription(title = "Remove link between a rule set and a folder node", + description = "Submits a request to unlink a rule set from a folder", + successStatus = HttpServletResponse.SC_NO_CONTENT) + @Override + public void delete(String folderNodeId, String ruleSetNodeId, Parameters parameters) + { + ruleSets.unlinkRuleSet(folderNodeId, ruleSetNodeId); + } + + public NodeRuleSetLinksRelation(RuleSets ruleSets) + { + this.ruleSets = ruleSets; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/rules/NodeRuleSetsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/rules/NodeRuleSetsRelation.java new file mode 100644 index 0000000000..00bb8117ce --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/rules/NodeRuleSetsRelation.java @@ -0,0 +1,124 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.rules; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.rest.api.RuleSets; +import org.alfresco.rest.api.model.rules.RuleSet; +import org.alfresco.rest.api.nodes.NodesEntityResource; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundException; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.Experimental; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * Folder node rule sets. + */ +@Experimental +@RelationshipResource(name = "rule-sets", entityResource = NodesEntityResource.class, title = "Folder node rule sets") +public class NodeRuleSetsRelation implements RelationshipResourceAction.Read, + RelationshipResourceAction.ReadById, + RelationshipResourceAction.Update, + InitializingBean +{ + private RuleSets ruleSets; + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "ruleSets", ruleSets); + } + + /** + * List rule sets for given folder. + *

+ * - GET /nodes/{folderNodeId}/rule-sets + * + * @param folderNodeId The id of the folder node. + * @param parameters Contains paging information and information about which fields to include + * @return {@link CollectionWithPagingInfo} containing a page of rule sets + */ + @WebApiDescription ( + title = "Get rule sets for a folder", + description = "Returns a paged list of rule sets for given node", + successStatus = HttpServletResponse.SC_OK + ) + @Override + public CollectionWithPagingInfo readAll(String folderNodeId, Parameters parameters) + { + return ruleSets.getRuleSets(folderNodeId, parameters.getInclude(), parameters.getPaging()); + } + + /** + * Get single folder rule for given node's, rule set's and rule's IDs. + *

+ * - GET /nodes/{folderNodeId}/rule-sets/{ruleSetId} + * + * @param folderNodeId - entity resource context for this relationship + * @param ruleSetId - rule set node ID (associated with folder node) + * @param parameters Contains information about which fields to include + * @return {@link RuleSet} definition + * @throws RelationshipResourceNotFoundException in case resource was not found + */ + @WebApiDescription ( + title = "Get rule set", + description = "Returns a single rule set for the given node", + successStatus = HttpServletResponse.SC_OK + ) + @Override + public RuleSet readById(String folderNodeId, String ruleSetId, Parameters parameters) throws RelationshipResourceNotFoundException + { + return ruleSets.getRuleSetById(folderNodeId, ruleSetId, parameters.getInclude()); + } + + public void setRuleSets(RuleSets ruleSets) + { + this.ruleSets = ruleSets; + } + + /** + * Update a rule set, in particular this is useful for reordering rules in a rule set. + *

+ * - PUT /nodes/{folderNodeId}/rule-sets/{ruleSetId} + * + * @param folderNodeId The id for the folder. + * @param ruleSet The updated rule set. + * @param parameters Contains information about which fields to include in the response. + * @return The updated rule set. + */ + @Override + public RuleSet update(String folderNodeId, RuleSet ruleSet, Parameters parameters) + { + return ruleSets.updateRuleSet(folderNodeId, ruleSet, parameters.getInclude()); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/rules/NodeRuleSettingsRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/rules/NodeRuleSettingsRelation.java new file mode 100644 index 0000000000..25ab9c8355 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/rules/NodeRuleSettingsRelation.java @@ -0,0 +1,105 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.rules; + +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.rest.api.RuleSettings; +import org.alfresco.rest.api.model.rules.RuleSetting; +import org.alfresco.rest.api.nodes.NodesEntityResource; +import org.alfresco.rest.framework.WebApiDescription; +import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundException; +import org.alfresco.rest.framework.resource.RelationshipResource; +import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.Experimental; +import org.alfresco.util.PropertyCheck; +import org.springframework.beans.factory.InitializingBean; + +/** + * Folder node rule settings (rule inheritance). + */ +@Experimental +@RelationshipResource (name = "rule-settings", entityResource = NodesEntityResource.class, title = "Folder rule settings") +public class NodeRuleSettingsRelation implements RelationshipResourceAction.ReadById, + RelationshipResourceAction.Update, + InitializingBean +{ + private RuleSettings ruleSettings; + + @Override + public void afterPropertiesSet() throws Exception + { + PropertyCheck.mandatory(this, "ruleSettings", ruleSettings); + } + + /** + * Get the given configuration value for the specified folder. + *

+ * - GET /nodes/{folderId}/rule-settings/{ruleSettingKey} + * + * @param folderId The id of the folder. + * @param ruleSettingKey The setting to retrieve. + * @param parameters Unused. + * @return {@link RuleSetting} The current value of the setting. + */ + @WebApiDescription ( + title = "Get a folder node rule setting", + description = "Returns the specified rule setting for the given folder", + successStatus = HttpServletResponse.SC_OK + ) + @Override + public RuleSetting readById(String folderId, String ruleSettingKey, Parameters parameters) throws RelationshipResourceNotFoundException + { + return ruleSettings.getRuleSetting(folderId, ruleSettingKey); + } + + /** + * Set the value of a rule setting for the specified folder. + *

+ * PUT /nodes/{folderId}/rule-settings/{ruleSettingKey} + * + * @param folderId The id of the folder. + * @param ruleSetting The new value of the rule setting. + * @param parameters Unused. + * @return The updated rule setting. + */ + @WebApiDescription ( + title = "Update folder node rule setting", + description = "Update a rule setting for given node", + successStatus = HttpServletResponse.SC_OK + ) + @Override + public RuleSetting update(String folderId, RuleSetting ruleSetting, Parameters parameters) + { + return ruleSettings.setRuleSetting(folderId, ruleSetting); + } + + public void setRuleSettings(RuleSettings ruleSettings) + { + this.ruleSettings = ruleSettings; + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRulesRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/rules/NodeRulesRelation.java similarity index 54% rename from remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRulesRelation.java rename to remote-api/src/main/java/org/alfresco/rest/api/rules/NodeRulesRelation.java index 2345825573..606ebbb21b 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/nodes/NodeRulesRelation.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/rules/NodeRulesRelation.java @@ -24,7 +24,10 @@ * #L% */ -package org.alfresco.rest.api.nodes; +package org.alfresco.rest.api.rules; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; import org.alfresco.rest.api.Rules; import org.alfresco.rest.api.model.rules.Rule; @@ -38,15 +41,18 @@ import org.alfresco.service.Experimental; import org.alfresco.util.PropertyCheck; import org.springframework.beans.factory.InitializingBean; -import javax.servlet.http.HttpServletResponse; - /** * Folder node's rules. * */ @Experimental @RelationshipResource(name = "rules", entityResource = NodeRuleSetsRelation.class, title = "Folder node rules") -public class NodeRulesRelation implements RelationshipResourceAction.Read, RelationshipResourceAction.ReadById, InitializingBean +public class NodeRulesRelation implements RelationshipResourceAction.Read, + RelationshipResourceAction.ReadById, + RelationshipResourceAction.Create, + RelationshipResourceAction.Update, + RelationshipResourceAction.Delete, + InitializingBean { private Rules rules; @@ -59,7 +65,7 @@ public class NodeRulesRelation implements RelationshipResourceAction.Read, /** * List folder rules for given folder node's and rule set's IDs as a page. - * + *

* - GET /nodes/{folderNodeId}/rule-sets/{ruleSetId}/rules * * @param folderNodeId - entity resource context for this relationship @@ -76,12 +82,12 @@ public class NodeRulesRelation implements RelationshipResourceAction.Read, { final String ruleSetId = parameters.getRelationshipId(); - return rules.getRules(folderNodeId, ruleSetId, parameters.getPaging()); + return rules.getRules(folderNodeId, ruleSetId, parameters.getInclude(), parameters.getPaging()); } /** * Get single folder rule for given node's, rule set's and rule's IDs. - * + *

* - GET /nodes/{folderNodeId}/rule-sets/{ruleSetId}/rules/{ruleId} * * @param folderNodeId - entity resource context for this relationship @@ -100,7 +106,76 @@ public class NodeRulesRelation implements RelationshipResourceAction.Read, { final String ruleId = parameters.getRelationship2Id(); - return rules.getRuleById(folderNodeId, ruleSetId, ruleId); + return rules.getRuleById(folderNodeId, ruleSetId, ruleId, parameters.getInclude()); + } + + /** + * Create one or more rules inside a given folder and rule set. + *

+ * POST /nodes/{folderNodeId}/rule-sets/{ruleSetId}/rules + * + * @param folderNodeId The folder in which to create the rule. + * @param ruleList The list of rules to create. + * @param parameters List of parameters including the rule set id as the relationship. + * @return The newly created rules. + */ + @WebApiDescription( + title = "Create folder rule", + description = "Creates one or more folder rules for the given folder and rule set", + successStatus = HttpServletResponse.SC_CREATED + ) + @Override + public List create(String folderNodeId, List ruleList, Parameters parameters) + { + final String ruleSetId = parameters.getRelationshipId(); + + return rules.createRules(folderNodeId, ruleSetId, ruleList, parameters.getInclude()); + } + + /** + * Update the specified folder rule. + *

+ * PUT /nodes/{folderNodeId}/rule-sets/{ruleSetId}/rules/{ruleId} + * + * @param folderNodeId The id of the folder containing the rule. + * @param rule The updated rule. + * @param parameters List of parameters including the rule set id and rule id. + * @return The updated rule. + * @throws RelationshipResourceNotFoundException in case resource was not found + */ + @WebApiDescription ( + title = "Update folder node rule", + description = "Update a single rule definition for given node's, rule set's and rule's IDs", + successStatus = HttpServletResponse.SC_OK + ) + @Override + public Rule update(String folderNodeId, Rule rule, Parameters parameters) + { + String ruleSetId = parameters.getRelationshipId(); + String ruleId = parameters.getRelationship2Id(); + return rules.updateRuleById(folderNodeId, ruleSetId, ruleId, rule, parameters.getInclude()); + } + + /** + * Delete single folder rule for given node's, rule set's and rule's IDs. + *

+ * - DELETE /nodes/{folderNodeId}/rule-sets/{ruleSetId}/rules/{ruleId} + * + * @param folderNodeId - entity resource context for this relationship + * @param ruleSetId - rule set node ID (associated with folder node) + * @param parameters - Should not be null. Should contain at least ruleId (relationship2Id) + * @throws RelationshipResourceNotFoundException in case resource was not found + */ + @WebApiDescription( + title="Delete folder node rule", + description = "Deletes a single rule definition for given node's, rule set's and rule's IDs", + successStatus = HttpServletResponse.SC_NO_CONTENT + ) + @Override + public void delete(String folderNodeId, String ruleSetId, Parameters parameters) + { + final String ruleId = parameters.getRelationship2Id(); + rules.deleteRuleById(folderNodeId, ruleSetId, ruleId); } public void setRules(Rules rules) diff --git a/remote-api/src/main/java/org/alfresco/rest/api/rules/package-info.java b/remote-api/src/main/java/org/alfresco/rest/api/rules/package-info.java new file mode 100644 index 0000000000..f2d0be9e65 --- /dev/null +++ b/remote-api/src/main/java/org/alfresco/rest/api/rules/package-info.java @@ -0,0 +1,29 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +@WebApi(name="alfresco", scope=Api.SCOPE.PRIVATE, version=1) +package org.alfresco.rest.api.rules; +import org.alfresco.rest.framework.Api; +import org.alfresco.rest.framework.WebApi; diff --git a/remote-api/src/main/java/org/alfresco/rest/framework/core/ResourceLookupDictionary.java b/remote-api/src/main/java/org/alfresco/rest/framework/core/ResourceLookupDictionary.java index 5c578cc586..c03f594a5e 100644 --- a/remote-api/src/main/java/org/alfresco/rest/framework/core/ResourceLookupDictionary.java +++ b/remote-api/src/main/java/org/alfresco/rest/framework/core/ResourceLookupDictionary.java @@ -121,8 +121,8 @@ public class ResourceLookupDictionary implements ResourceLocator return resource; } } - logger.warn("Unable to locate resource resource for :"+entityResource+" "+relationResource==null?"":relationResource+" "+property==null?"":property); - throw new NotFoundException("Unable to locate resource resource for :"+entityResource+" "+(relationResource==null?"":relationResource+" "+property==null?"":property)); + logger.warn("Unable to locate resource for: "+entityResource+" "+relationResource==null ? "" : relationResource+" "+property==null ? "" : property); + throw new NotFoundException("Unable to locate resource for: "+entityResource+" "+(relationResource==null ? "" : relationResource+" "+property==null ? "" : property)); } else { @@ -160,8 +160,8 @@ public class ResourceLookupDictionary implements ResourceLocator return resource; } } - logger.warn("Unable to locate resource resource for :"+entityResource+" "+relationResource==null?"":relationResource); - throw new NotFoundException("Unable to locate resource resource for :"+entityResource+" "+(relationResource==null?"":relationResource)); + logger.warn("Unable to locate resource for: "+entityResource+" "+relationResource==null ? "" : relationResource); + throw new NotFoundException("Unable to locate resource for: "+entityResource+" "+relationResource==null ? "" : relationResource); } else { diff --git a/remote-api/src/main/java/org/alfresco/rest/framework/core/exceptions/EntityNotFoundException.java b/remote-api/src/main/java/org/alfresco/rest/framework/core/exceptions/EntityNotFoundException.java index 488ed3f498..e053e1da13 100644 --- a/remote-api/src/main/java/org/alfresco/rest/framework/core/exceptions/EntityNotFoundException.java +++ b/remote-api/src/main/java/org/alfresco/rest/framework/core/exceptions/EntityNotFoundException.java @@ -34,7 +34,7 @@ public class EntityNotFoundException extends NotFoundException { private static final long serialVersionUID = -1198595000441207734L; public static String DEFAULT_MESSAGE_ID = "framework.exception.EntityNotFound"; - + /** * The entity id param will be shown in the default error message. * @param entityId String @@ -43,4 +43,20 @@ public class EntityNotFoundException extends NotFoundException { super(DEFAULT_MESSAGE_ID, new String[] {entityId}); } + + /** + * The entity id param will be shown in the default error message. + * + * @param msgId The message template. + * @param parameters The message template parameters. + */ + public EntityNotFoundException(String msgId, String[] parameters) + { + super(msgId, parameters); + } + + public EntityNotFoundException(String msgId, Throwable cause) + { + super(msgId, cause); + } } diff --git a/remote-api/src/main/java/org/alfresco/rest/framework/core/exceptions/NotFoundException.java b/remote-api/src/main/java/org/alfresco/rest/framework/core/exceptions/NotFoundException.java index 62c0bcb26d..0b4e674c8c 100644 --- a/remote-api/src/main/java/org/alfresco/rest/framework/core/exceptions/NotFoundException.java +++ b/remote-api/src/main/java/org/alfresco/rest/framework/core/exceptions/NotFoundException.java @@ -53,4 +53,9 @@ public class NotFoundException extends ApiException super(msgId, notFoundObjects); } + public NotFoundException(String msgId, Throwable cause) + { + super(msgId, cause); + } + } diff --git a/remote-api/src/main/java/org/alfresco/rest/framework/webscripts/ResourceWebScriptPut.java b/remote-api/src/main/java/org/alfresco/rest/framework/webscripts/ResourceWebScriptPut.java index 24e6e385c4..9a63cef648 100644 --- a/remote-api/src/main/java/org/alfresco/rest/framework/webscripts/ResourceWebScriptPut.java +++ b/remote-api/src/main/java/org/alfresco/rest/framework/webscripts/ResourceWebScriptPut.java @@ -79,6 +79,7 @@ public class ResourceWebScriptPut extends AbstractResourceWebScript implements P final Map resourceVars = locator.parseTemplateVars(req.getServiceMatch().getTemplateVars()); final String entityId = resourceVars.get(ResourceLocator.ENTITY_ID); final String relationshipId = resourceVars.get(ResourceLocator.RELATIONSHIP_ID); + final String relationship2Id = resourceVars.get(ResourceLocator.RELATIONSHIP2_ID); final RecognizedParams params = getRecognizedParams(req); final ResourceOperation operation = resourceMeta.getOperation(HttpMethod.PUT); @@ -99,9 +100,16 @@ public class ResourceWebScriptPut extends AbstractResourceWebScript implements P if (StringUtils.isBlank(relationshipId)) { throw new UnsupportedResourceOperationException("PUT is executed against the instance URL"); - } else + } + Object putRel = extractJsonContent(req, assistant.getJsonHelper(), resourceMeta.getObjectType(operation)); + if (StringUtils.isNotBlank(relationship2Id)) + { + ResourceWebScriptHelper.setUniqueId(putRel, relationship2Id); + return Params.valueOf(false, entityId, relationshipId, relationship2Id, + putRel, null, null, params, null, req); + } + else { - Object putRel = extractJsonContent(req, assistant.getJsonHelper(), resourceMeta.getObjectType(operation)); ResourceWebScriptHelper.setUniqueId(putRel,relationshipId); return Params.valueOf(entityId, params, putRel, req); } diff --git a/remote-api/src/main/resources/alfresco/public-rest-context.xml b/remote-api/src/main/resources/alfresco/public-rest-context.xml index a8a1efd97a..ab2b53b4bb 100644 --- a/remote-api/src/main/resources/alfresco/public-rest-context.xml +++ b/remote-api/src/main/resources/alfresco/public-rest-context.xml @@ -155,6 +155,7 @@ + @@ -566,6 +567,7 @@ + @@ -583,7 +585,23 @@ - + + + + + + + + + + + + + + + + + org.alfresco.rest.api.Downloads @@ -850,10 +868,69 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -866,8 +943,56 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/remote-api/src/main/resources/alfresco/remote-api-context.xml b/remote-api/src/main/resources/alfresco/remote-api-context.xml index 3e6d8ec8d1..26cb10a816 100644 --- a/remote-api/src/main/resources/alfresco/remote-api-context.xml +++ b/remote-api/src/main/resources/alfresco/remote-api-context.xml @@ -8,6 +8,7 @@ + diff --git a/remote-api/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/forms/pickerchildren.get.js b/remote-api/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/forms/pickerchildren.get.js index 577f024530..9db0a6ff6c 100644 --- a/remote-api/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/forms/pickerchildren.get.js +++ b/remote-api/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/forms/pickerchildren.get.js @@ -309,7 +309,7 @@ function sortByName(a, b) function findUsers(searchTerm, maxResults, results) { - var personRefs = people.getPeople(searchTerm, maxResults, "lastName", true); + var personRefs = people.getPeople(searchTerm+ " [hint:useCQ]", maxResults, "lastName", true); // create person object for each result for each(var personRef in personRefs) diff --git a/remote-api/src/main/resources/alfresco/templates/webscripts/org/springframework/extensions/webscripts/index.get.html.ftl b/remote-api/src/main/resources/alfresco/templates/webscripts/org/springframework/extensions/webscripts/index.get.html.ftl new file mode 100644 index 0000000000..cd1004a03c --- /dev/null +++ b/remote-api/src/main/resources/alfresco/templates/webscripts/org/springframework/extensions/webscripts/index.get.html.ftl @@ -0,0 +1,82 @@ +<#import "/org/springframework/extensions/webscripts/webscripts.lib.html.ftl" as wsLib/> + + +<@wsLib.head>${msg("alfresco.index.title")} + +

+ <@wsLib.indexheader>Web Scripts Home + <#if failures?size > 0> +
+ + +
(+${failures?size} failed)
+ +
+ <@wsLib.onlinedoc/> +
+ Index + <#if rootfamily.children?size > 0> + + <#list rootfamily.children as childpath> + + +
Browse '${childpath.name}' Web Scripts
+
+ + + + + + +
Browse all Web Scripts
Browse by Web Script URI
Browse by Web Script Package
Browse by Web Script Lifecycle
+
+
+ Maintenance +
+ + + <#if failures?size > 0> + + + +
Browse failed Web Scripts
Alfresco Javascript Debugger
+
+ + +
+
+
+<#assign CSRF=(config.scoped["CSRFPolicy"]["filter"].getChildren("rule")?size != 0)!false> +<#if CSRF> + + + + \ No newline at end of file diff --git a/remote-api/src/main/resources/alfresco/web-scripts-application-context.xml b/remote-api/src/main/resources/alfresco/web-scripts-application-context.xml index 47d5358de3..4da5da51b0 100644 --- a/remote-api/src/main/resources/alfresco/web-scripts-application-context.xml +++ b/remote-api/src/main/resources/alfresco/web-scripts-application-context.xml @@ -249,6 +249,7 @@ + diff --git a/remote-api/src/test/java/org/alfresco/AppContextExtraTestSuite.java b/remote-api/src/test/java/org/alfresco/AppContextExtraTestSuite.java index ddd6508f0f..4dcdda433a 100644 --- a/remote-api/src/test/java/org/alfresco/AppContextExtraTestSuite.java +++ b/remote-api/src/test/java/org/alfresco/AppContextExtraTestSuite.java @@ -48,9 +48,8 @@ import org.junit.runners.Suite; org.alfresco.repo.webdav.RenameShuffleDetectionTest.class, org.alfresco.repo.webdav.WebDAVHelperTest.class, org.alfresco.repo.webdav.WebDAVLockServiceImplTest.class, + org.alfresco.rest.api.RulesUnitTests.class, org.alfresco.rest.api.impl.ContentStorageInformationImplTest.class, - org.alfresco.rest.api.impl.RulesImplTest.class, - org.alfresco.rest.api.nodes.NodeRulesRelationTest.class, org.alfresco.rest.api.nodes.NodeStorageInfoRelationTest.class, org.alfresco.rest.api.search.ResultMapperTests.class, org.alfresco.rest.api.search.SearchApiWebscriptTests.class, @@ -100,6 +99,7 @@ import org.junit.runners.Suite; org.alfresco.repo.webdav.WebDAVMethodTest.class, org.alfresco.repo.webdav.PutMethodTest.class, org.alfresco.repo.webdav.WebDAVonContentUpdateTest.class, + org.alfresco.repo.webdav.WebDAVInsecurePostMethodTest.class, // [classpath:test-rest-context.xml] org.alfresco.rest.framework.tests.core.ExceptionResolverTests.class, diff --git a/remote-api/src/test/java/org/alfresco/repo/web/scripts/content/ContentGetTest.java b/remote-api/src/test/java/org/alfresco/repo/web/scripts/content/ContentGetTest.java index 3e0e2f0f0d..0141fa3a0e 100644 --- a/remote-api/src/test/java/org/alfresco/repo/web/scripts/content/ContentGetTest.java +++ b/remote-api/src/test/java/org/alfresco/repo/web/scripts/content/ContentGetTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Share Services AMP * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -66,13 +66,17 @@ public class ContentGetTest extends BaseWebScriptTest { super.setUp(); - this.authenticationService = (MutableAuthenticationService) getServer().getApplicationContext() - .getBean("AuthenticationService"); + this.authenticationService = (MutableAuthenticationService)getServer().getApplicationContext().getBean("AuthenticationService"); this.personService = (PersonService) getServer().getApplicationContext().getBean("PersonService"); this.nodeService = (NodeService) getServer().getApplicationContext().getBean("NodeService"); this.contentService = (ContentService) getServer().getApplicationContext().getBean("ContentService"); AuthenticationUtil.setAdminUserAsFullyAuthenticatedUser(); createUser(USER_ONE); + + Repository repositoryHelper = (Repository) getServer().getApplicationContext().getBean("repositoryHelper"); + NodeRef companyHome = repositoryHelper.getCompanyHome(); + + rootFolder = createNode(companyHome, "rootFolder", ContentModel.TYPE_FOLDER); } private void createUser(String userName) @@ -117,18 +121,13 @@ public class ContentGetTest extends BaseWebScriptTest */ public void testRelativePath() throws Exception { - Repository repositoryHelper = (Repository) getServer().getApplicationContext().getBean("repositoryHelper"); - NodeRef companyHome = repositoryHelper.getCompanyHome(); - - rootFolder = createNode(companyHome, "rootFolder", ContentModel.TYPE_FOLDER); - - NodeRef doc1 = createNodeWithTextContent(rootFolder, "doc1", ContentModel.TYPE_CONTENT, "doc1 file content"); + NodeRef doc1 = createNodeWithTextContent(rootFolder, "doc1", ContentModel.TYPE_CONTENT, "doc1 file content", MimetypeMap.MIMETYPE_TEXT_PLAIN); NodeRef folderX = createNode(rootFolder, "X", ContentModel.TYPE_FOLDER); NodeRef folderY = createNode(folderX, "Y", ContentModel.TYPE_FOLDER); NodeRef folderZ = createNode(folderY, "Z", ContentModel.TYPE_FOLDER); - NodeRef doc2 = createNodeWithTextContent(folderZ, "doc2", ContentModel.TYPE_CONTENT, "doc2 file content"); + NodeRef doc2 = createNodeWithTextContent(folderZ, "doc2", ContentModel.TYPE_CONTENT, "doc2 file content", MimetypeMap.MIMETYPE_TEXT_PLAIN); // uri with relative path at the end String uri = URL_CONTENT_DOWNLOAD + doc1.getId() + "/X/Y/Z/doc2"; @@ -138,7 +137,46 @@ public class ContentGetTest extends BaseWebScriptTest Assert.assertEquals("doc2 file content", resp.getContentAsString()); } - public NodeRef createNodeWithTextContent(NodeRef parentNode, String nodeCmName, QName nodeType, String content) + + + public void testForcedAttachment() throws Exception + { + NodeRef testhtml = createNodeWithTextContent(rootFolder, "testhtml", ContentModel.TYPE_CONTENT, "testhtml content", MimetypeMap.MIMETYPE_HTML); + NodeRef testpdf = createNodeWithTextContent(rootFolder, "testpdf", ContentModel.TYPE_CONTENT, "testpdf content", MimetypeMap.MIMETYPE_PDF); + + String uri = URL_CONTENT_DOWNLOAD + testhtml.getId() + "?a=false"; + GetRequest req = new GetRequest(uri); + Response res = sendRequest(req, 200); + assertEquals("attachment", res.getHeader("Content-Disposition")); + assertEquals(MimetypeMap.MIMETYPE_HTML + ";charset=UTF-8", res.getContentType()); + + uri = URL_CONTENT_DOWNLOAD + testhtml.getId(); + res = sendRequest(new GetRequest(uri), 200); + assertEquals("attachment", res.getHeader("Content-Disposition")); + assertEquals(MimetypeMap.MIMETYPE_HTML + ";charset=UTF-8", res.getContentType()); + + uri = URL_CONTENT_DOWNLOAD + testhtml.getId() + "?a=true"; + res = sendRequest(new GetRequest(uri), 200); + assertEquals("attachment", res.getHeader("Content-Disposition")); + assertEquals(MimetypeMap.MIMETYPE_HTML + ";charset=UTF-8", res.getContentType()); + + uri = URL_CONTENT_DOWNLOAD + testpdf.getId() + "?a=false"; + res = sendRequest(new GetRequest(uri), 200); + assertNull(res.getHeader("Content-Disposition")); + assertEquals(MimetypeMap.MIMETYPE_PDF + ";charset=UTF-8", res.getContentType()); + + uri = URL_CONTENT_DOWNLOAD + testpdf.getId(); + res = sendRequest(new GetRequest(uri), 200); + assertNull(res.getHeader("Content-Disposition")); + assertEquals(MimetypeMap.MIMETYPE_PDF + ";charset=UTF-8", res.getContentType()); + + uri = URL_CONTENT_DOWNLOAD + testpdf.getId() + "?a=true"; + res = sendRequest(new GetRequest(uri), 200); + assertEquals("attachment", res.getHeader("Content-Disposition")); + assertEquals(MimetypeMap.MIMETYPE_PDF + ";charset=UTF-8", res.getContentType()); + } + + public NodeRef createNodeWithTextContent(NodeRef parentNode, String nodeCmName, QName nodeType, String content, String mimetype) { NodeRef nodeRef = createNode(parentNode, nodeCmName, nodeType); @@ -146,7 +184,7 @@ public class ContentGetTest extends BaseWebScriptTest if (content != null) { ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true); - writer.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN); + writer.setMimetype(mimetype); writer.setEncoding("UTF-8"); writer.putContent(content); } diff --git a/remote-api/src/test/java/org/alfresco/repo/web/scripts/preference/PreferenceServiceTest.java b/remote-api/src/test/java/org/alfresco/repo/web/scripts/preference/PreferenceServiceTest.java index abd5faaeb4..ea20595684 100644 --- a/remote-api/src/test/java/org/alfresco/repo/web/scripts/preference/PreferenceServiceTest.java +++ b/remote-api/src/test/java/org/alfresco/repo/web/scripts/preference/PreferenceServiceTest.java @@ -55,10 +55,14 @@ public class PreferenceServiceTest extends BaseWebScriptTest private PersonService personService; private static final String USER_ONE = "PreferenceTestOne" + System.currentTimeMillis(); - + private static final String USER_TWO = "PreferenceTestTwo" + System.currentTimeMillis(); private static final String USER_BAD = "PreferenceTestBad" + System.currentTimeMillis(); - private static final String URL = "/api/people/" + USER_ONE + "/preferences";; + private static final String URL = "/api/people/" + USER_ONE + "/preferences"; + private static final String URL2 = "/api/people/" + USER_TWO + "/preferences"; + + private static final int NUM_THREADS = 3; + private static Integer THREADS_FINISHED = 0; @Override protected void setUp() throws Exception @@ -75,6 +79,7 @@ public class PreferenceServiceTest extends BaseWebScriptTest // Create users createUser(USER_ONE); + createUser(USER_TWO); createUser(USER_BAD); // Do tests as user one @@ -103,7 +108,6 @@ public class PreferenceServiceTest extends BaseWebScriptTest { super.tearDown(); this.authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName()); - } public void testPreferences() throws Exception @@ -193,6 +197,64 @@ public class PreferenceServiceTest extends BaseWebScriptTest sendRequest(new GetRequest("/api/people/noExistUser/preferences"), 404); } + public void testPreferencesMNT21901() throws Exception + { + String[] body = { + "{\"org\":{\"alfresco\":{\"share\":{\"forum\":{\"summary\":{\"dashlet\":{\"component-1-5\":{\"topics\":\"mine\"}}}}}}}}", + "{\"org\":{\"alfresco\":{\"share\":{\"forum\":{\"summary\":{\"dashlet\":{\"component-2-5\":{\"topics\":\"mine\"}}}}}}}}", + "{\"org\":{\"alfresco\":{\"share\":{\"forum\":{\"summary\":{\"dashlet\":{\"component-2-5\":{\"history\":\"1\"}}}}}}}}" + }; + + Thread[] threads = new Thread[NUM_THREADS]; + + for (int i = 0; i < NUM_THREADS; i++) + { + UpdatePreferencesThread t = new UpdatePreferencesThread(i, body[i]); + threads[i] = t; + t.start(); + } + + for (int i = 0; i < threads.length; i++) + { + threads[i].join(); + } + + authenticationComponent.setCurrentUser(USER_ONE); + + if (THREADS_FINISHED != NUM_THREADS) + { + fail("An error has occurred when updating preferences in concurrency context"); + } + } + + private class UpdatePreferencesThread extends Thread + { + private String body; + + UpdatePreferencesThread(int threadNum, String body) + { + super(UpdatePreferencesThread.class.getSimpleName() + "-" + threadNum); + this.body = body; + } + + @Override + public void run() + { + authenticationComponent.setCurrentUser(USER_TWO); + + try + { + Response resp = sendRequest(new PostRequest(URL2, body, "application/json"), 200); + assertEquals(0, resp.getContentLength()); + THREADS_FINISHED++; + } + catch (Exception e) + { + // Intentionally empty + } + } + } + private JSONObject getPreferenceObj() throws JSONException { JSONObject jsonObject = new JSONObject(); @@ -208,5 +270,4 @@ public class PreferenceServiceTest extends BaseWebScriptTest assertEquals(10, jsonObject.get("numberValue")); assertEquals(BigDecimal.valueOf(3.142), jsonObject.get("numberValue2")); } - } diff --git a/remote-api/src/test/java/org/alfresco/repo/web/util/AbstractJettyComponent.java b/remote-api/src/test/java/org/alfresco/repo/web/util/AbstractJettyComponent.java index 7f74b09a73..fe03e82b96 100644 --- a/remote-api/src/test/java/org/alfresco/repo/web/util/AbstractJettyComponent.java +++ b/remote-api/src/test/java/org/alfresco/repo/web/util/AbstractJettyComponent.java @@ -1,28 +1,28 @@ -/* - * #%L - * Alfresco Remote API - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.repo.web.util; import java.io.BufferedReader; @@ -32,6 +32,7 @@ import java.io.InputStreamReader; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; +import java.util.Arrays; import java.util.Date; import javax.servlet.ServletContext; @@ -44,7 +45,8 @@ import org.alfresco.util.TempFileProvider; import org.alfresco.util.WebApplicationContextLoader; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.eclipse.jetty.security.HashLoginService; +import org.eclipse.jetty.http.UriCompliance; +import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.webapp.WebAppContext; import org.springframework.beans.BeanUtils; @@ -145,6 +147,8 @@ public abstract class AbstractJettyComponent implements JettyComponent configureWebAppContext(webAppContext); + ignoreAmbiguousLinks(server); + server.start(); if(logger.isDebugEnabled()) @@ -203,15 +207,27 @@ public abstract class AbstractJettyComponent implements JettyComponent } } }); - - // with a login-config in web.xml, jetty seems to require this in order to start successfully - webAppContext.getSecurityHandler().setLoginService(new HashLoginService()); // arbitrary temporary file location File tmp = new File(TempFileProvider.getSystemTempDir(), String.valueOf(System.currentTimeMillis())); webAppContext.setResourceBase(tmp.getAbsolutePath()); } + /** + * In newer jetty versions there is a stricter check for links e.g. "//" is not allowed, which clashes + * with some of our tests, because even a NodeRef triggers it - "workspace://..." + * Since Jetty is only used in tests it's alright to block this behaviour. + * + * @param server + */ + private void ignoreAmbiguousLinks(Server server) { + Arrays.stream(server.getConnectors()) + .flatMap(c -> c.getConnectionFactories().stream()) + .filter(cf -> cf instanceof HttpConnectionFactory) + .map(cf -> (HttpConnectionFactory) cf) + .forEach(hcf -> hcf.getHttpConfiguration().setUriCompliance(UriCompliance.RFC3986)); + } + public void shutdown() { try diff --git a/remote-api/src/test/java/org/alfresco/repo/webdav/WebDAVInsecurePostMethodTest.java b/remote-api/src/test/java/org/alfresco/repo/webdav/WebDAVInsecurePostMethodTest.java new file mode 100644 index 0000000000..c86e7cda73 --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/repo/webdav/WebDAVInsecurePostMethodTest.java @@ -0,0 +1,131 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.repo.webdav; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Hashtable; + +import org.alfresco.repo.webdav.WebDAVServlet.WebDAVInitParameters; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * Tests for the allowInsecurePOSTMethod flag. + * + * @see WebDAVInitParameters + * @author Aleksandra Onych + */ +@RunWith(MockitoJUnitRunner.class) +public class WebDAVInsecurePostMethodTest +{ + private WebDAVServlet davServlet; + private @Mock WebDAVInitParameters webDAVInitParameters; + private @Mock HttpServletRequest request; + private @Mock HttpServletResponse response; + private @Mock Hashtable> davMethods; + + @Before + public void setUp() + { + davServlet = new WebDAVServlet(); + ReflectionTestUtils.setField(davServlet, "initParams", webDAVInitParameters); + ReflectionTestUtils.setField(davServlet, "m_davMethods", davMethods); + when(webDAVInitParameters.getEnabled()).thenReturn(true); + } + + + @Test + public void shouldReturn405StatusWhenPostMethodIsNotAllowed() throws ServletException, IOException + { + prepareRequest(WebDAV.METHOD_POST); + when(webDAVInitParameters.allowInsecurePOSTMethod()).thenReturn(false); + + davServlet.service(request, response); + + verify(response).sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } + + @Test + public void shouldNotReturn405StatusWhenPostMethodIsAllowed() throws ServletException, IOException + { + prepareRequest(WebDAV.METHOD_POST); + when(webDAVInitParameters.allowInsecurePOSTMethod()).thenReturn(true); + + davServlet.service(request, response); + + verify(response, never()).sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } + + @Test + public void shouldNotReturn405StatusForPutMethod() throws ServletException, IOException + { + prepareRequest(WebDAV.METHOD_PUT); + + davServlet.service(request, response); + + verify(response, never()).sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } + + @Test + public void shouldNotReturn405StatusForGetMethod() throws ServletException, IOException + { + prepareRequest(WebDAV.METHOD_GET); + + davServlet.service(request, response); + + verify(response, never()).sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } + + @Test + public void shouldNotReturn405StatusForDeleteMethod() throws ServletException, IOException + { + prepareRequest(WebDAV.METHOD_DELETE); + + davServlet.service(request, response); + + verify(response, never()).sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } + + private void prepareRequest(String requestMethodName) + { + doReturn(PutMethod.class).when(davMethods).get(requestMethodName); + when(request.getMethod()).thenReturn(requestMethodName); + } +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/RulesUnitTests.java b/remote-api/src/test/java/org/alfresco/rest/api/RulesUnitTests.java new file mode 100644 index 0000000000..85e1266353 --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/RulesUnitTests.java @@ -0,0 +1,67 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api; + +import org.alfresco.rest.api.impl.mapper.rules.RestRuleActionModelMapperTest; +import org.alfresco.rest.api.impl.mapper.rules.RestRuleCompositeConditionModelMapperTest; +import org.alfresco.rest.api.impl.mapper.rules.RestRuleModelMapperTest; +import org.alfresco.rest.api.impl.mapper.rules.RestRuleSimpleConditionModelMapperTest; +import org.alfresco.rest.api.impl.rules.ActionParameterConverterTest; +import org.alfresco.rest.api.impl.rules.ActionPermissionValidatorTest; +import org.alfresco.rest.api.impl.rules.NodeValidatorTest; +import org.alfresco.rest.api.impl.rules.RuleLoaderTest; +import org.alfresco.rest.api.impl.rules.RuleSetsImplTest; +import org.alfresco.rest.api.impl.rules.RulesImplTest; +import org.alfresco.rest.api.impl.validator.actions.ActionNodeParameterValidatorTest; +import org.alfresco.rest.api.impl.validator.actions.ActionParameterDefinitionValidatorTest; +import org.alfresco.rest.api.rules.NodeRuleSetsRelationTest; +import org.alfresco.rest.api.rules.NodeRulesRelationTest; +import org.alfresco.service.Experimental; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@Experimental +@RunWith(Suite.class) +@Suite.SuiteClasses({ + NodeRulesRelationTest.class, + NodeRuleSetsRelationTest.class, + RulesImplTest.class, + RuleSetsImplTest.class, + NodeValidatorTest.class, + RuleLoaderTest.class, + ActionParameterConverterTest.class, + ActionPermissionValidatorTest.class, + ActionParameterDefinitionValidatorTest.class, + ActionNodeParameterValidatorTest.class, + RestRuleSimpleConditionModelMapperTest.class, + RestRuleCompositeConditionModelMapperTest.class, + RestRuleActionModelMapperTest.class, + RestRuleModelMapperTest.class +}) +public class RulesUnitTests +{ +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/actions/ActionConstraintsEntityResourceTest.java b/remote-api/src/test/java/org/alfresco/rest/api/actions/ActionConstraintsEntityResourceTest.java new file mode 100644 index 0000000000..16447288e1 --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/actions/ActionConstraintsEntityResourceTest.java @@ -0,0 +1,66 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.actions; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import org.alfresco.rest.api.Actions; +import org.alfresco.rest.api.model.ActionParameterConstraint; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ActionConstraintsEntityResourceTest +{ + @Mock + private Actions actionsMock; + @Mock + private Parameters parametersMock; + + @InjectMocks + private ActionConstraintsEntityResource objectUnderTest; + + @Test + public void testReadById() { + final String name = "name"; + final ActionParameterConstraint dummyConstraint = new ActionParameterConstraint(); + given(actionsMock.getActionConstraint(name)).willReturn(dummyConstraint); + + //when + ActionParameterConstraint result = objectUnderTest.readById(name, parametersMock); + + then(actionsMock).should().getActionConstraint(name); + then(actionsMock).shouldHaveNoMoreInteractions(); + assertThat(result).isNotNull().usingRecursiveComparison().isEqualTo(dummyConstraint); + } +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/actions/ActionParameterConstraintsEntityResourceTest.java b/remote-api/src/test/java/org/alfresco/rest/api/actions/ActionParameterConstraintsEntityResourceTest.java new file mode 100644 index 0000000000..7a582c17c6 --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/actions/ActionParameterConstraintsEntityResourceTest.java @@ -0,0 +1,66 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.actions; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import org.alfresco.rest.api.Actions; +import org.alfresco.rest.api.model.ActionParameterConstraint; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ActionParameterConstraintsEntityResourceTest +{ + @Mock + private Actions actionsMock; + @Mock + private Parameters parametersMock; + + @InjectMocks + private ActionParameterConstraintsEntityResource objectUnderTest; + + @Test + public void testReadById() { + final String name = "name"; + final ActionParameterConstraint dummyConstraint = new ActionParameterConstraint(); + given(actionsMock.getActionConstraint(name)).willReturn(dummyConstraint); + + //when + ActionParameterConstraint result = objectUnderTest.readById(name, parametersMock); + + then(actionsMock).should().getActionConstraint(name); + then(actionsMock).shouldHaveNoMoreInteractions(); + assertThat(result).isNotNull().usingRecursiveComparison().isEqualTo(dummyConstraint); + } +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/ActionsImplTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/ActionsImplTest.java new file mode 100644 index 0000000000..ef07315b43 --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/ActionsImplTest.java @@ -0,0 +1,165 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl; + +import static org.alfresco.rest.api.impl.ActionsImpl.CONSTRAINT_NOT_EXISTS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.util.Map; + +import org.alfresco.repo.action.constraint.FolderContentsParameterConstraint; +import org.alfresco.rest.api.impl.rules.ActionParameterConverter; +import org.alfresco.rest.api.model.ActionParameterConstraint; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.action.ParameterConstraint; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ActionsImplTest +{ + private static final String NAME = "name"; + private static final String CONSTRAINT = "constraint"; + private static final String LABEL = "label"; + private static final String DISPLAY = "display "; + + @Mock + private ActionService actionServiceMock; + @Mock + private Parameters parametersMock; + @Mock + private ActionParameterConverter parameterConverterMock; + + @InjectMocks + private ActionsImpl objectUnderTest; + + @Test + public void testGetSingleActionConstraint() + { + final String name = "name"; + final String value = CONSTRAINT; + final String label = LABEL; + final Map values = Map.of(value, label); + final ParameterConstraint testConstraint = createTestConstraint(name, values); + given(actionServiceMock.getParameterConstraint(name)).willReturn(testConstraint); + + //when + final ActionParameterConstraint actualConstraint = objectUnderTest.getActionConstraint(name); + + then(parametersMock).shouldHaveNoInteractions(); + then(actionServiceMock).should().getParameterConstraint(name); + then(actionServiceMock).shouldHaveNoMoreInteractions(); + assertThat(actualConstraint).isNotNull(); + assertThat(actualConstraint.getConstraintName()).isEqualTo(testConstraint.getName()); + ActionParameterConstraint.ConstraintData expectedConstraintData = new ActionParameterConstraint.ConstraintData(value, label); + assertThat(actualConstraint.getConstraintValues()).isNotNull().hasSize(1); + ActionParameterConstraint.ConstraintData actualConstraintData = actualConstraint.getConstraintValues().get(0); + assertThat(actualConstraintData).usingRecursiveComparison().isEqualTo(expectedConstraintData); + } + + @Test + public void testGetSingleActionNodeConstraint() + { + final String name = "name1"; + final String dummyNodeId = "dummy-node-id"; + final String value = "workspace://DummyStore/" + dummyNodeId; + final Map values = Map.of(value, LABEL); + final FolderContentsParameterConstraint testConstraint = mock(FolderContentsParameterConstraint.class); + given(testConstraint.getName()).willReturn(name); + given(testConstraint.getValues()).willReturn(values); + given(actionServiceMock.getParameterConstraint(name)).willReturn(testConstraint); + given(parameterConverterMock.convertParamFromServiceModel(any())).willReturn(dummyNodeId); + + //when + final ActionParameterConstraint actualConstraint = objectUnderTest.getActionConstraint(name); + + then(parametersMock).shouldHaveNoInteractions(); + then(actionServiceMock).should().getParameterConstraint(name); + then(actionServiceMock).shouldHaveNoMoreInteractions(); + assertThat(actualConstraint).isNotNull(); + assertThat(actualConstraint.getConstraintName()).isEqualTo(testConstraint.getName()); + ActionParameterConstraint.ConstraintData expectedConstraintData = new ActionParameterConstraint.ConstraintData(dummyNodeId, LABEL); + assertThat(actualConstraint.getConstraintValues()).isNotNull().hasSize(1); + ActionParameterConstraint.ConstraintData actualConstraintData = actualConstraint.getConstraintValues().get(0); + assertThat(actualConstraintData).usingRecursiveComparison().isEqualTo(expectedConstraintData); + } + + @Test + public void testGetActionConstraintsWithNameFilterNonExistingConstraint() + { + final String name = "name"; + given(actionServiceMock.getParameterConstraint(name)).willReturn(null); + + //when + assertThatExceptionOfType(NotFoundException.class).isThrownBy(() -> objectUnderTest.getActionConstraint(name)) + .withMessageContaining(String.format(CONSTRAINT_NOT_EXISTS, name)); + + then(parametersMock).shouldHaveNoInteractions(); + then(actionServiceMock).should().getParameterConstraint(name); + then(actionServiceMock).shouldHaveNoMoreInteractions(); + } + + private ParameterConstraint createTestConstraint(final String name, final Map values) + { + return new ParameterConstraint() + { + @Override + public String getName() + { + return name; + } + + @Override + public boolean isValidValue(String value) + { + return true; + } + + @Override + public String getValueDisplayLabel(String value) + { + return DISPLAY + name; + } + + @Override + public Map getAllowableValues() + { + return values; + } + }; + } +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/RulesImplTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/RulesImplTest.java deleted file mode 100644 index ad50bf30a5..0000000000 --- a/remote-api/src/test/java/org/alfresco/rest/api/impl/RulesImplTest.java +++ /dev/null @@ -1,290 +0,0 @@ -/* - * #%L - * Alfresco Remote API - * %% - * Copyright (C) 2005 - 2022 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ - -package org.alfresco.rest.api.impl; - -import junit.framework.TestCase; -import org.alfresco.rest.api.Nodes; -import org.alfresco.rest.api.model.rules.Rule; -import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; -import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; -import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; -import org.alfresco.rest.framework.resource.parameters.Paging; -import org.alfresco.service.Experimental; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.StoreRef; -import org.alfresco.service.cmr.rule.RuleService; -import org.alfresco.service.cmr.security.AccessStatus; -import org.alfresco.service.cmr.security.PermissionService; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.junit.MockitoJUnitRunner; - -import java.util.Collection; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.then; - -@Experimental -@RunWith(MockitoJUnitRunner.class) -public class RulesImplTest extends TestCase -{ - - private static final String FOLDER_NODE_ID = "dummy-folder-node-id"; - private static final String RULE_SET_ID = "dummy-rule-set-id"; - private static final String RULE_ID = "dummy-rule-id"; - private static final NodeRef folderNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, FOLDER_NODE_ID); - private static final NodeRef ruleSetNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, RULE_SET_ID); - private static final NodeRef ruleNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, RULE_ID); - private static final Paging paging = Paging.DEFAULT; - - @Mock - private Nodes nodesMock; - - @Mock - private PermissionService permissionServiceMock; - - @Mock - private RuleService ruleServiceMock; - - @InjectMocks - private RulesImpl rules; - - @Before - @Override - public void setUp() throws Exception - { - MockitoAnnotations.openMocks(this); - - given(nodesMock.validateOrLookupNode(eq(FOLDER_NODE_ID), any())).willReturn(folderNodeRef); - given(nodesMock.validateNode(eq(RULE_SET_ID))).willReturn(ruleSetNodeRef); - given(nodesMock.nodeMatches(any(), any(), any())).willReturn(true); - given(permissionServiceMock.hasReadPermission(any())).willReturn(AccessStatus.ALLOWED); - } - - @Test - public void testGetRules() - { - given(ruleServiceMock.isRuleSetAssociatedWithFolder(any(), any())).willReturn(true); - given(ruleServiceMock.getRules(any())).willReturn(List.of(createRule(RULE_ID))); - - // when - final CollectionWithPagingInfo rulesPage = rules.getRules(FOLDER_NODE_ID, RULE_SET_ID, paging); - - then(nodesMock).should().validateOrLookupNode(eq(FOLDER_NODE_ID), isNull()); - then(nodesMock).should().validateNode(eq(RULE_SET_ID)); - then(nodesMock).should().nodeMatches(eq(folderNodeRef), any(), isNull()); - then(nodesMock).should().nodeMatches(eq(ruleSetNodeRef), any(), isNull()); - then(nodesMock).shouldHaveNoMoreInteractions(); - then(permissionServiceMock).should().hasReadPermission(eq(folderNodeRef)); - then(permissionServiceMock).shouldHaveNoMoreInteractions(); - then(ruleServiceMock).should().isRuleSetAssociatedWithFolder(eq(ruleSetNodeRef), eq(folderNodeRef)); - then(ruleServiceMock).should().getRules(eq(folderNodeRef)); - then(ruleServiceMock).shouldHaveNoMoreInteractions(); - assertThat(rulesPage) - .isNotNull() - .extracting(CollectionWithPagingInfo::getCollection) - .isNotNull() - .extracting(Collection::size) - .isEqualTo(1); - assertThat(rulesPage.getCollection().stream().findFirst().orElse(null)) - .isNotNull() - .extracting(Rule::getId) - .isEqualTo(RULE_ID); - } - - @Test - public void testGetRulesForDefaultRuleSet() - { - final String defaultRuleSetId = "-default-"; - given(ruleServiceMock.getRules(any())).willReturn(List.of(createRule(RULE_ID))); - - // when - final CollectionWithPagingInfo rulesPage = rules.getRules(FOLDER_NODE_ID, defaultRuleSetId, paging); - - then(nodesMock).should().validateOrLookupNode(eq(FOLDER_NODE_ID), isNull()); - then(nodesMock).should().nodeMatches(eq(folderNodeRef), any(), isNull()); - then(nodesMock).shouldHaveNoMoreInteractions(); - then(permissionServiceMock).should().hasReadPermission(eq(folderNodeRef)); - then(permissionServiceMock).shouldHaveNoMoreInteractions(); - then(ruleServiceMock).should().getRuleSetNode(eq(folderNodeRef)); - then(ruleServiceMock).should().getRules(eq(folderNodeRef)); - then(ruleServiceMock).shouldHaveNoMoreInteractions(); - assertThat(rulesPage) - .isNotNull() - .extracting(CollectionWithPagingInfo::getCollection) - .isNotNull() - .extracting(Collection::size) - .isEqualTo(1); - assertThat(rulesPage.getCollection().stream().findFirst().orElse(null)) - .isNotNull() - .extracting(Rule::getId) - .isEqualTo(RULE_ID); - } - - @Test - public void testGetRulesForNotExistingFolderNode() - { - given(nodesMock.nodeMatches(eq(folderNodeRef), any(), any())).willReturn(false); - - // when - assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy( - () -> rules.getRules(FOLDER_NODE_ID, RULE_SET_ID, paging)); - - then(ruleServiceMock).shouldHaveNoInteractions(); - } - - @Test - public void testGetRulesForNotExistingRuleSetNode() - { - given(nodesMock.nodeMatches(eq(folderNodeRef), any(), any())).willReturn(true); - given(nodesMock.nodeMatches(eq(ruleSetNodeRef), any(), any())).willReturn(false); - - // when - assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy( - () -> rules.getRules(FOLDER_NODE_ID, RULE_SET_ID, paging)); - - then(ruleServiceMock).shouldHaveNoInteractions(); - } - - @Test - public void testGetRulesForNotAssociatedRuleSetToFolder() - { - given(ruleServiceMock.isRuleSetAssociatedWithFolder(any(), any())).willReturn(false); - - // when - assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy( - () -> rules.getRules(FOLDER_NODE_ID, RULE_SET_ID, paging)); - - then(ruleServiceMock).should().isRuleSetAssociatedWithFolder(eq(ruleSetNodeRef), eq(folderNodeRef)); - then(ruleServiceMock).shouldHaveNoMoreInteractions(); - } - - @Test - public void testGetRulesWithoutReadPermission() - { - given(permissionServiceMock.hasReadPermission(any())).willReturn(AccessStatus.DENIED); - - // when - assertThatExceptionOfType(PermissionDeniedException.class).isThrownBy( - () -> rules.getRules(FOLDER_NODE_ID, RULE_SET_ID, paging)); - - then(ruleServiceMock).shouldHaveNoInteractions(); - } - - @Test - public void testGetRuleById() - { - given(nodesMock.validateNode(eq(RULE_ID))).willReturn(ruleNodeRef); - given(ruleServiceMock.isRuleSetAssociatedWithFolder(any(), any())).willReturn(true); - given(ruleServiceMock.isRuleAssociatedWithRuleSet(any(), any())).willReturn(true); - given(ruleServiceMock.getRule(any())).willReturn(createRule(RULE_ID)); - - // when - final Rule rule = rules.getRuleById(FOLDER_NODE_ID, RULE_SET_ID, RULE_ID); - - then(nodesMock).should().validateOrLookupNode(eq(FOLDER_NODE_ID), isNull()); - then(nodesMock).should().validateNode(eq(RULE_SET_ID)); - then(nodesMock).should().validateNode(eq(RULE_ID)); - then(nodesMock).should().nodeMatches(eq(folderNodeRef), any(), isNull()); - then(nodesMock).should().nodeMatches(eq(ruleSetNodeRef), any(), isNull()); - then(nodesMock).should().nodeMatches(eq(ruleNodeRef), any(), isNull()); - then(nodesMock).shouldHaveNoMoreInteractions(); - then(permissionServiceMock).should().hasReadPermission(eq(folderNodeRef)); - then(permissionServiceMock).shouldHaveNoMoreInteractions(); - then(ruleServiceMock).should().isRuleSetAssociatedWithFolder(eq(ruleSetNodeRef), eq(folderNodeRef)); - then(ruleServiceMock).should().isRuleAssociatedWithRuleSet(eq(ruleNodeRef), eq(ruleSetNodeRef)); - then(ruleServiceMock).should().getRule(eq(ruleNodeRef)); - then(ruleServiceMock).shouldHaveNoMoreInteractions(); - assertThat(rule) - .isNotNull() - .extracting(Rule::getId) - .isEqualTo(RULE_ID); - } - - @Test - public void testGetRuleByIdForDefaultRuleSet() - { - final String defaultRuleSetId = "-default-"; - given(nodesMock.validateNode(eq(RULE_ID))).willReturn(ruleNodeRef); - given(ruleServiceMock.getRuleSetNode(any())).willReturn(ruleSetNodeRef); - given(ruleServiceMock.isRuleAssociatedWithRuleSet(any(), any())).willReturn(true); - given(ruleServiceMock.getRule(any())).willReturn(createRule(RULE_ID)); - - // when - final Rule rule = rules.getRuleById(FOLDER_NODE_ID, defaultRuleSetId, RULE_ID); - - then(nodesMock).should().validateOrLookupNode(eq(FOLDER_NODE_ID), isNull()); - then(nodesMock).should().validateNode(eq(RULE_ID)); - then(nodesMock).should().nodeMatches(eq(folderNodeRef), any(), isNull()); - then(nodesMock).should().nodeMatches(eq(ruleNodeRef), any(), isNull()); - then(nodesMock).shouldHaveNoMoreInteractions(); - then(permissionServiceMock).should().hasReadPermission(eq(folderNodeRef)); - then(permissionServiceMock).shouldHaveNoMoreInteractions(); - then(ruleServiceMock).should().getRuleSetNode(eq(folderNodeRef)); - then(ruleServiceMock).should().isRuleAssociatedWithRuleSet(eq(ruleNodeRef), eq(ruleSetNodeRef)); - then(ruleServiceMock).should().getRule(eq(ruleNodeRef)); - then(ruleServiceMock).shouldHaveNoMoreInteractions(); - assertThat(rule) - .isNotNull() - .extracting(Rule::getId) - .isEqualTo(RULE_ID); - } - - @Test - public void testGetRuleByIdForNotAssociatedRuleToRuleSet() - { - given(nodesMock.validateNode(eq(RULE_SET_ID))).willReturn(ruleSetNodeRef); - given(nodesMock.validateNode(eq(RULE_ID))).willReturn(ruleNodeRef); - given(ruleServiceMock.isRuleSetAssociatedWithFolder(any(), any())).willReturn(true); - given(ruleServiceMock.isRuleAssociatedWithRuleSet(any(), any())).willReturn(false); - - // when - assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy( - () -> rules.getRuleById(FOLDER_NODE_ID, RULE_SET_ID, RULE_ID)); - - then(ruleServiceMock).should().isRuleSetAssociatedWithFolder(eq(ruleSetNodeRef), eq(folderNodeRef)); - then(ruleServiceMock).should().isRuleAssociatedWithRuleSet(eq(ruleNodeRef), eq(ruleSetNodeRef)); - then(ruleServiceMock).shouldHaveNoMoreInteractions(); - } - - private static org.alfresco.service.cmr.rule.Rule createRule(final String id) { - final org.alfresco.service.cmr.rule.Rule rule = new org.alfresco.service.cmr.rule.Rule(); - rule.setNodeRef(new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, id)); - - return rule; - } -} \ No newline at end of file diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleActionModelMapperTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleActionModelMapperTest.java new file mode 100644 index 0000000000..234a975a35 --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleActionModelMapperTest.java @@ -0,0 +1,148 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.mapper.rules; + +import static java.util.Collections.emptyMap; + +import static org.alfresco.repo.action.access.ActionAccessRestriction.ACTION_CONTEXT_PARAM_NAME; +import static org.alfresco.repo.action.executer.SetPropertyValueActionExecuter.PARAM_PROPERTY; +import static org.alfresco.repo.action.executer.SetPropertyValueActionExecuter.PARAM_VALUE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.rest.api.actions.ActionValidator; +import org.alfresco.rest.api.impl.rules.ActionParameterConverter; +import org.alfresco.rest.api.model.rules.Action; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@Experimental +@RunWith(MockitoJUnitRunner.class) +public class RestRuleActionModelMapperTest +{ + + private static final String ACTION_DEFINITION_NAME = "actionDefName"; + private static final Map parameters = + Map.of(PARAM_PROPERTY, "propertyName", PARAM_VALUE, "propertyValue", ACTION_CONTEXT_PARAM_NAME, "rule"); + + @Mock + private ActionParameterConverter parameterConverter; + @Mock + private ActionValidator sampleValidatorMock; + + private RestRuleActionModelMapper objectUnderTest; + + @Before + public void setUp() { + objectUnderTest = new RestRuleActionModelMapper(parameterConverter, List.of(sampleValidatorMock)); + } + + @Test + public void testToRestModel() + { + final NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "ruleId"); + final org.alfresco.service.cmr.action.Action actionServiceModel = + new ActionImpl(nodeRef, "actionId", ACTION_DEFINITION_NAME, parameters); + given(parameterConverter.convertParamFromServiceModel(any())).willAnswer(a -> a.getArgument(0)); + + //when + final Action actualAction = objectUnderTest.toRestModel(actionServiceModel); + + then(parameterConverter).should(times(3)).convertParamFromServiceModel(any()); + then(parameterConverter).shouldHaveNoMoreInteractions(); + final Map expectedParameters = Map.of(PARAM_PROPERTY, "propertyName", PARAM_VALUE, "propertyValue"); + final Action expectedAction = Action.builder().actionDefinitionId(ACTION_DEFINITION_NAME).params(expectedParameters).create(); + assertThat(actualAction).isNotNull().usingRecursiveComparison().isEqualTo(expectedAction); + } + + @Test + public void testToRestModelWithNullValues() + { + final org.alfresco.service.cmr.action.Action actionServiceModel = new ActionImpl(null, null, null); + final Action expectedAction = Action.builder().params(emptyMap()).create(); + + //when + final Action actualAction = objectUnderTest.toRestModel(actionServiceModel); + + then(parameterConverter).shouldHaveNoInteractions(); + assertThat(actualAction).isNotNull().usingRecursiveComparison().isEqualTo(expectedAction); + } + + @Test + public void testToServiceModel() { + final Action action = Action.builder().actionDefinitionId(ACTION_DEFINITION_NAME).params(parameters).create(); + final List actions = List.of(action); + given(parameterConverter.getConvertedParams(parameters, ACTION_DEFINITION_NAME)).willAnswer(a -> a.getArgument(0)); + + //when + final org.alfresco.service.cmr.action.Action serviceModelAction = objectUnderTest.toServiceModel(actions); + then(parameterConverter).should().getConvertedParams(parameters, ACTION_DEFINITION_NAME); + then(parameterConverter).shouldHaveNoMoreInteractions(); + assertThat(serviceModelAction).isNotNull(); + } + + @Test + public void testToServiceModelFromEmptyActions() { + final List actions = Collections.emptyList(); + + //when + final org.alfresco.service.cmr.action.Action serviceModelAction = objectUnderTest.toServiceModel(actions); + + then(parameterConverter).shouldHaveNoInteractions(); + assertThat(serviceModelAction).isNull(); + } + + @Test + public void testToServiceModelWithNullParams() + { + final Action action = Action.builder().actionDefinitionId(ACTION_DEFINITION_NAME).params(null).create(); + final List actions = List.of(action); + + //when + final org.alfresco.service.cmr.action.Action serviceModelAction = objectUnderTest.toServiceModel(actions); + then(parameterConverter).should().getConvertedParams(emptyMap(), ACTION_DEFINITION_NAME); + then(parameterConverter).shouldHaveNoMoreInteractions(); + assertThat(serviceModelAction).isNotNull(); + } +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleCompositeConditionModelMapperTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleCompositeConditionModelMapperTest.java new file mode 100644 index 0000000000..a75822a614 --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleCompositeConditionModelMapperTest.java @@ -0,0 +1,251 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.mapper.rules; + +import static org.alfresco.repo.action.evaluator.NoConditionEvaluator.NAME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +import org.alfresco.repo.action.ActionConditionImpl; +import org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator; +import org.alfresco.rest.api.model.mapper.RestModelMapper; +import org.alfresco.rest.api.model.rules.CompositeCondition; +import org.alfresco.rest.api.model.rules.ConditionOperator; +import org.alfresco.rest.api.model.rules.SimpleCondition; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.action.ActionCondition; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@Experimental +@RunWith(MockitoJUnitRunner.class) +public class RestRuleCompositeConditionModelMapperTest +{ + + @Mock + private RestModelMapper simpleConditionMapperMock; + + @InjectMocks + private RestRuleCompositeConditionModelMapper objectUnderTest; + + @Test + public void testToRestModel() + { + final List actionConditions = List.of( + createActionCondition("value1"), + createActionCondition("value3"), + createActionCondition("value2", true) + ); + final List simpleConditions = List.of( + createSimpleCondition("value1"), + createSimpleCondition("value3"), + createSimpleCondition("value2") + ); + + final CompositeCondition expectedCompositeCondition = createCompositeCondition(List.of( + createCompositeCondition(false, simpleConditions.subList(0,2)), + createCompositeCondition(true, simpleConditions.subList(2,3)) + )); + given(simpleConditionMapperMock.toRestModels(actionConditions.subList(2,3))).willReturn(simpleConditions.subList(2,3)); + given(simpleConditionMapperMock.toRestModels(actionConditions.subList(0,2))).willReturn(simpleConditions.subList(0,2)); + + // when + final CompositeCondition actualCompositeCondition = objectUnderTest.toRestModel(actionConditions); + + assertThat(actualCompositeCondition).isNotNull().usingRecursiveComparison().isEqualTo(expectedCompositeCondition); + } + + @Test + public void testToRestModel_fromEmptyList() + { + final List actionConditions = Collections.emptyList(); + + // when + final CompositeCondition actualCompositeCondition = objectUnderTest.toRestModel(actionConditions); + + assertThat(actualCompositeCondition).isNull(); + } + + @Test + public void testToRestModel_fromNullValue() + { + // when + final CompositeCondition actualCompositeCondition = objectUnderTest.toRestModel((Collection) null); + + assertThat(actualCompositeCondition).isNull(); + } + + @Test + public void testToRestModel_fromListContainingNullsOnly() + { + final List actionConditions = new ArrayList<>(); + actionConditions.add(null); + actionConditions.add(null); + + // when + final CompositeCondition actualCompositeCondition = objectUnderTest.toRestModel(actionConditions); + + assertThat(actualCompositeCondition).isNull(); + } + + @Test + public void testToRestModel_fromNoCondition() + { + final List actionConditions = new ArrayList<>(); + final ActionCondition noCondition = new ActionConditionImpl("fake-id", NAME); + actionConditions.add(noCondition); + + // when + final CompositeCondition actualCompositeCondition = objectUnderTest.toRestModel(actionConditions); + + assertThat(actualCompositeCondition).isNull(); + } + + @Test + public void testToServiceModels() { + final List simpleConditions = List.of( + createSimpleCondition("value1"), + createSimpleCondition("value3"), + createSimpleCondition("value2") + ); + final CompositeCondition compositeCondition = createCompositeCondition(List.of( + createCompositeCondition(false, simpleConditions.subList(0,2)), + createCompositeCondition(true, simpleConditions.subList(2,3)) + )); + final List actionConditions = List.of( + createActionCondition("value1"), + createActionCondition("value3"), + createActionCondition("value2", true) + ); + + IntStream.rangeClosed(0, 2) + .forEach(i -> given(simpleConditionMapperMock.toServiceModel(simpleConditions.get(i))).willReturn(actionConditions.get(i))); + + final List actualActionConditions = objectUnderTest.toServiceModels(compositeCondition); + assertThat(actualActionConditions).isEqualTo(actionConditions); + } + + @Test + public void testToServiceModels_simpleNonInvertedConditionsOnly() { + final List simpleConditions = List.of( + createSimpleCondition("value1"), + createSimpleCondition("value2"), + createSimpleCondition("value3") + ); + final CompositeCondition compositeCondition = createCompositeCondition(false, simpleConditions); + final List actionConditions = List.of( + createActionCondition("value1"), + createActionCondition("value2"), + createActionCondition("value3") + ); + + IntStream.rangeClosed(0, 2) + .forEach(i -> given(simpleConditionMapperMock.toServiceModel(simpleConditions.get(i))).willReturn(actionConditions.get(i))); + + final List actualActionConditions = objectUnderTest.toServiceModels(compositeCondition); + assertThat(actualActionConditions).isEqualTo(actionConditions); + } + + @Test + public void testToServiceModels_nullSimpleConditions() { + final CompositeCondition compositeCondition = createCompositeCondition(false, null); + + final List actualActionConditions = objectUnderTest.toServiceModels(compositeCondition); + assertThat(actualActionConditions).isNotNull().isEmpty(); + } + + @Test + public void testToServiceModels_emptyCompositeCondition() { + final CompositeCondition compositeCondition = CompositeCondition.builder().create(); + + final List actualActionConditions = objectUnderTest.toServiceModels(compositeCondition); + assertThat(actualActionConditions).isNotNull().isEmpty(); + } + + @Test + public void testToServiceModels_nullCompositeCondition() { + final CompositeCondition compositeCondition = null; + + final List actualActionConditions = objectUnderTest.toServiceModels(compositeCondition); + assertThat(actualActionConditions).isNotNull().isEmpty(); + } + + private static ActionCondition createActionCondition(final String value) + { + return createActionCondition(value, false); + } + + private static ActionCondition createActionCondition(final String value, final boolean inverted) + { + final ActionCondition actionCondition = new ActionConditionImpl("fake-id", ComparePropertyValueEvaluator.NAME); + actionCondition.setInvertCondition(inverted); + final Map parameterValues = new HashMap<>(); + parameterValues.put(ComparePropertyValueEvaluator.PARAM_CONTENT_PROPERTY, "content-property"); + parameterValues.put(ComparePropertyValueEvaluator.PARAM_OPERATION, "operation"); + parameterValues.put(ComparePropertyValueEvaluator.PARAM_VALUE, value); + actionCondition.setParameterValues(parameterValues); + return actionCondition; + } + + private static SimpleCondition createSimpleCondition(final String value) { + return SimpleCondition.builder() + .field("content-property") + .comparator("operation") + .parameter(value) + .create(); + } + + private static CompositeCondition createCompositeCondition(final List compositeConditions) { + return createCompositeCondition(false, ConditionOperator.AND, compositeConditions, null); + } + + private static CompositeCondition createCompositeCondition(final boolean inverted, final List simpleConditions) { + return createCompositeCondition(inverted, ConditionOperator.AND, null, simpleConditions); + } + + private static CompositeCondition createCompositeCondition(final boolean inverted, final ConditionOperator conditionOperator, + final List compositeConditions, final List simpleConditions) { + return CompositeCondition.builder() + .inverted(inverted) + .booleanMode(conditionOperator) + .compositeConditions(compositeConditions) + .simpleConditions(simpleConditions) + .create(); + } +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleModelMapperTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleModelMapperTest.java new file mode 100644 index 0000000000..bcf7eacf78 --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleModelMapperTest.java @@ -0,0 +1,244 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.mapper.rules; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import java.util.List; + +import org.alfresco.repo.action.ActionConditionImpl; +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.repo.action.CompositeActionImpl; +import org.alfresco.repo.action.executer.ScriptActionExecuter; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.impl.rules.ActionParameterConverter; +import org.alfresco.rest.api.model.rules.Action; +import org.alfresco.rest.api.model.rules.CompositeCondition; +import org.alfresco.rest.api.model.rules.Rule; +import org.alfresco.rest.api.model.rules.RuleTrigger; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.action.CompositeAction; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.rule.RuleType; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@Experimental +@RunWith(MockitoJUnitRunner.class) +public class RestRuleModelMapperTest +{ + private static final String RULE_ID = "fake-rule-id"; + private static final String RULE_NAME = "rule name"; + private static final String RULE_DESCRIPTION = "rule description"; + private static final boolean RULE_ENABLED = true; + private static final boolean RULE_INHERITABLE = true; + private static final boolean RULE_ASYNC = true; + private static final String ACTION_DEFINITION_NAME = "action-def-name"; + private static final String ERROR_SCRIPT = "error-script-ref"; + + @Mock + private RestRuleActionModelMapper actionMapperMock; + @Mock + private RestRuleCompositeConditionModelMapper compositeConditionMapperMock; + @Mock + private Nodes nodesMock; + @Mock + private ActionParameterConverter actionParameterConverterMock; + + private RestRuleModelMapper objectUnderTest; + + @Before + public void setUp() + { + objectUnderTest = new RestRuleModelMapper(compositeConditionMapperMock, actionMapperMock, nodesMock, actionParameterConverterMock); + } + + @Test + public void testToRestModel() + { + final org.alfresco.service.cmr.rule.Rule ruleModel = createRuleModel(); + given(actionParameterConverterMock.convertParamFromServiceModel(any())).willAnswer(a -> a.getArgument(0)); + given(actionMapperMock.toRestModel(createActionModel())).willReturn(createAction()); + given(compositeConditionMapperMock.toRestModel(List.of(createConditionModel()))).willReturn(createCondition()); + + // when + final Rule actualRule = objectUnderTest.toRestModel(ruleModel); + + then(compositeConditionMapperMock).should().toRestModel(ruleModel.getAction().getActionConditions()); + then(compositeConditionMapperMock).shouldHaveNoMoreInteractions(); + then(actionParameterConverterMock).should().convertParamFromServiceModel(ERROR_SCRIPT); + then(actionParameterConverterMock).shouldHaveNoMoreInteractions(); + ((CompositeAction) ruleModel.getAction()).getActions().forEach(a -> then(actionMapperMock).should().toRestModel(a)); + final Rule expectedRule = createRuleWithDefaultValues(); + assertThat(actualRule).isNotNull().usingRecursiveComparison().isEqualTo(expectedRule); + } + + @Test + public void testToRestModelWithNullValues() + { + final org.alfresco.service.cmr.rule.Rule ruleModel = new org.alfresco.service.cmr.rule.Rule(); + final Rule expectedRule = Rule.builder().isEnabled(true).create(); + + // when + final Rule actualRule = objectUnderTest.toRestModel(ruleModel); + + assertThat(actualRule).isNotNull().usingRecursiveComparison().isEqualTo(expectedRule); + + } + + @Test + public void testToServiceModel() + { + final Rule rule = createRuleWithDefaultValues(); + final Action action = Action.builder().actionDefinitionId(ACTION_DEFINITION_NAME).create(); + rule.setActions(List.of(action)); + final CompositeCondition compositeCondition = CompositeCondition.builder().create(); + final org.alfresco.service.cmr.rule.Rule expectedRuleModel = createRuleModel(); + rule.setConditions(compositeCondition); + final org.alfresco.service.cmr.action.Action actionModel = createCompositeActionModel(); + given(actionMapperMock.toServiceModel(List.of(action))).willReturn(actionModel); + given(compositeConditionMapperMock.toServiceModels(compositeCondition)).willCallRealMethod(); + given(actionParameterConverterMock.getConvertedParams(any(), any())).willAnswer(a -> a.getArgument(0)); + // when + final org.alfresco.service.cmr.rule.Rule actualRuleModel = objectUnderTest.toServiceModel(rule); + + then(nodesMock).should().validateOrLookupNode(RULE_ID, null); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(actionMapperMock).should().toServiceModel(List.of(action)); + then(actionMapperMock).shouldHaveNoMoreInteractions(); + then(compositeConditionMapperMock).should().toServiceModels(compositeCondition); + then(compositeConditionMapperMock).shouldHaveNoMoreInteractions(); + assertThat(actualRuleModel) + .isNotNull() + .usingRecursiveComparison().ignoringFields("nodeRef", "action") + .isEqualTo(expectedRuleModel); + assertThat(actualRuleModel.getAction()) + .isNotNull(); + final org.alfresco.service.cmr.action.Action expectedCompensatingActionModel = createCompensatingActionModel(); + assertThat(actualRuleModel.getAction().getCompensatingAction()) + .isNotNull() + .usingRecursiveComparison().ignoringFields("id") + .isEqualTo(expectedCompensatingActionModel); + } + + @Test + public void testToServiceModel_withNullValues() + { + final Rule rule = new Rule(); + final org.alfresco.service.cmr.rule.Rule expectedRuleModel = new org.alfresco.service.cmr.rule.Rule(); + expectedRuleModel.setRuleDisabled(true); + + // when + final org.alfresco.service.cmr.rule.Rule actualRuleModel = objectUnderTest.toServiceModel(rule); + + then(nodesMock).shouldHaveNoInteractions(); + assertThat(actualRuleModel) + .isNotNull() + .usingRecursiveComparison() + .ignoringFields("ruleTypes") + .isEqualTo(expectedRuleModel); + } + + private Rule createRuleWithDefaultValues() + { + return Rule.builder() + .id(RULE_ID) + .name(RULE_NAME) + .description(RULE_DESCRIPTION) + .isEnabled(RULE_ENABLED) + .isInheritable(RULE_INHERITABLE) + .isAsynchronous(RULE_ASYNC) + .triggers(List.of(RuleTrigger.INBOUND, RuleTrigger.UPDATE)) + .errorScript(ERROR_SCRIPT) + .actions(List.of(createAction())) + .conditions(createCondition()) + .create(); + } + + private CompositeCondition createCondition() + { + return CompositeCondition.builder().create(); + } + + private Action createAction() { + return Action.builder().actionDefinitionId(ACTION_DEFINITION_NAME).create(); + } + + private static org.alfresco.service.cmr.rule.Rule createRuleModel() + { + final NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, RULE_ID); + final org.alfresco.service.cmr.rule.Rule ruleModel = new org.alfresco.service.cmr.rule.Rule(nodeRef); + ruleModel.setTitle(RULE_NAME); + ruleModel.setDescription(RULE_DESCRIPTION); + ruleModel.setRuleDisabled(!RULE_ENABLED); + ruleModel.applyToChildren(RULE_INHERITABLE); + ruleModel.setExecuteAsynchronously(RULE_ASYNC); + ruleModel.setRuleTypes(List.of(RuleType.INBOUND, RuleType.UPDATE)); + ruleModel.setAction(createCompositeActionModel()); + + return ruleModel; + } + + private static org.alfresco.service.cmr.action.Action createCompositeActionModel() + { + final ActionCondition actionCondition = createConditionModel(); + final org.alfresco.service.cmr.action.CompositeAction compositeActionModel = new CompositeActionImpl(null, "composite-action"); + compositeActionModel.addAction(createActionModel()); + compositeActionModel.setCompensatingAction(createCompensatingActionModel()); + compositeActionModel.addActionCondition(actionCondition); + + return compositeActionModel; + } + + private static ActionConditionImpl createConditionModel() + { + return new ActionConditionImpl("action-condition-id", "action-condition-def-name"); + } + + private static ActionImpl createActionModel() + { + return new ActionImpl(null, "action-id", ACTION_DEFINITION_NAME); + } + + private static org.alfresco.service.cmr.action.Action createCompensatingActionModel() + { + final org.alfresco.service.cmr.action.Action compensatingActionModel = + new ActionImpl(null, "compensating-action-id", ScriptActionExecuter.NAME); + compensatingActionModel.setParameterValue(ScriptActionExecuter.PARAM_SCRIPTREF, ERROR_SCRIPT); + + return compensatingActionModel; + } + +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleSimpleConditionModelMapperTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleSimpleConditionModelMapperTest.java new file mode 100644 index 0000000000..6a502a1224 --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/mapper/rules/RestRuleSimpleConditionModelMapperTest.java @@ -0,0 +1,441 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.mapper.rules; + +import static org.alfresco.rest.api.impl.mapper.rules.RestRuleSimpleConditionModelMapper.COMPARATOR_NOT_NULL; +import static org.alfresco.rest.api.impl.mapper.rules.RestRuleSimpleConditionModelMapper.FIELD_NOT_NULL; +import static org.alfresco.rest.api.impl.mapper.rules.RestRuleSimpleConditionModelMapper.PARAMETER_NOT_NULL; +import static org.alfresco.rest.api.impl.mapper.rules.RestRuleSimpleConditionModelMapper.PARAM_CATEGORY; +import static org.alfresco.rest.api.impl.mapper.rules.RestRuleSimpleConditionModelMapper.PARAM_MIMETYPE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.ActionConditionImpl; +import org.alfresco.repo.action.evaluator.CompareMimeTypeEvaluator; +import org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator; +import org.alfresco.repo.action.evaluator.HasAspectEvaluator; +import org.alfresco.repo.action.evaluator.HasChildEvaluator; +import org.alfresco.repo.action.evaluator.HasTagEvaluator; +import org.alfresco.repo.action.evaluator.HasVersionHistoryEvaluator; +import org.alfresco.repo.action.evaluator.InCategoryEvaluator; +import org.alfresco.repo.action.evaluator.IsSubTypeEvaluator; +import org.alfresco.repo.action.evaluator.NoConditionEvaluator; +import org.alfresco.repo.action.evaluator.compare.ComparePropertyValueOperation; +import org.alfresco.repo.action.evaluator.compare.ContentPropertyName; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.rules.SimpleCondition; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.action.ActionCondition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@Experimental +@RunWith(MockitoJUnitRunner.class) +public class RestRuleSimpleConditionModelMapperTest +{ + private static final boolean NULL_RESULT = true; + private static final String PARAMETER_DEFAULT = "value"; + + @Mock + private NamespaceService namespaceServiceMock; + @Mock + private Nodes nodesMock; + + @InjectMocks + private RestRuleSimpleConditionModelMapper objectUnderTest; + + @Before + public void setUp() throws Exception + { + given(namespaceServiceMock.getPrefixes(NamespaceService.CONTENT_MODEL_1_0_URI)).willReturn(List.of(NamespaceService.CONTENT_MODEL_PREFIX)); + given(namespaceServiceMock.getNamespaceURI(NamespaceService.CONTENT_MODEL_PREFIX)).willReturn(NamespaceService.CONTENT_MODEL_1_0_URI); + given(namespaceServiceMock.getPrefixes(NamespaceService.AUDIO_MODEL_1_0_URI)).willReturn(List.of(NamespaceService.AUDIO_MODEL_PREFIX)); + given(namespaceServiceMock.getNamespaceURI(NamespaceService.AUDIO_MODEL_PREFIX)).willReturn(NamespaceService.AUDIO_MODEL_1_0_URI); + } + + @Test + public void testToRestModel() + { + for (TestData testData : getTestData()) + { + final ActionCondition actionCondition = createActionCondition(testData.conditionDefinitionName); + + // when + final SimpleCondition actualSimpleCondition = objectUnderTest.toRestModel(actionCondition); + + assertThat(Objects.isNull(actualSimpleCondition)).isEqualTo(testData.isNullResult); + if (!testData.isNullResult) + { + assertThat(actualSimpleCondition.getField()).isNotEmpty(); + assertThat(actualSimpleCondition.getComparator()).isNotEmpty(); + assertThat(actualSimpleCondition.getParameter()).isNotEmpty(); + } + } + } + + @Test + public void testToRestModelFromNullValue() + { + // when + final ActionCondition actionCondition = null; + final SimpleCondition actualSimpleCondition = objectUnderTest.toRestModel(actionCondition); + + assertThat(actualSimpleCondition).isNull(); + } + + @Test + public void testToRestModelFromActionConditionWithoutDefinitionName() + { + final ActionCondition actionCondition = new ActionConditionImpl("fake-id", null, createParameterValues()); + + // when + final SimpleCondition actualSimpleCondition = objectUnderTest.toRestModel(actionCondition); + + assertThat(actualSimpleCondition).isNull(); + } + + @Test + public void testToRestModelFromActionConditionWithoutParameterValues() + { + final ActionCondition actionCondition = new ActionConditionImpl("fake-id", "fake-def-name", null); + + // when + final SimpleCondition actualSimpleCondition = objectUnderTest.toRestModel(actionCondition); + + assertThat(actualSimpleCondition).isNull(); + } + + @Test + public void testToRestModelListOfEmptyActionConditions() + { + // when + final List actualSimpleConditions = objectUnderTest.toRestModels(Collections.emptyList()); + + assertThat(actualSimpleConditions).isEmpty(); + } + + @Test + public void testToRestModelListOfNullActionConditions() + { + // when + assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> objectUnderTest.toRestModels( + (Collection) null)); + } + + @Test + public void testToRestModelListOfActionConditionsContainingNull() + { + final List actionConditions = new ArrayList<>(); + actionConditions.add(null); + + // when + final List actualSimpleConditions = objectUnderTest.toRestModels(actionConditions); + + assertThat(actualSimpleConditions).isEmpty(); + } + + @Test + public void testToServiceModel_withSizeContentProperty() + { + final SimpleCondition simpleCondition = createSimpleCondition(ContentPropertyName.SIZE.toString().toLowerCase()); + + // when + final ActionCondition actualActionCondition = objectUnderTest.toServiceModel(simpleCondition); + + final Map expectedParameterValues = new HashMap<>(); + expectedParameterValues.put(ComparePropertyValueEvaluator.PARAM_CONTENT_PROPERTY, ContentPropertyName.SIZE.toString()); + expectedParameterValues.put(ComparePropertyValueEvaluator.PARAM_PROPERTY, ContentModel.TYPE_CONTENT); + expectedParameterValues.put(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.EQUALS.toString()); + expectedParameterValues.put(ComparePropertyValueEvaluator.PARAM_VALUE, PARAMETER_DEFAULT); + ActionCondition expectedActionCondition = new ActionConditionImpl(null, ComparePropertyValueEvaluator.NAME, expectedParameterValues); + assertThat(actualActionCondition) + .isNotNull().usingRecursiveComparison().ignoringFields("id") + .isEqualTo(expectedActionCondition); + } + + @Test + public void testToServiceModel_withoutContentProperty() + { + final String field = NamespaceService.CONTENT_MODEL_PREFIX + QName.NAMESPACE_PREFIX + ContentModel.PROP_DESCRIPTION.toPrefixString(); + final SimpleCondition simpleCondition = createSimpleCondition(field); + + // when + final ActionCondition actualActionCondition = objectUnderTest.toServiceModel(simpleCondition); + + final Map expectedParameterValues = new HashMap<>(); + expectedParameterValues.put(ComparePropertyValueEvaluator.PARAM_PROPERTY, ContentModel.PROP_DESCRIPTION); + expectedParameterValues.put(ComparePropertyValueEvaluator.PARAM_OPERATION, ComparePropertyValueOperation.EQUALS.toString()); + expectedParameterValues.put(ComparePropertyValueEvaluator.PARAM_VALUE, PARAMETER_DEFAULT); + final ActionCondition expectedActionCondition = new ActionConditionImpl(null, ComparePropertyValueEvaluator.NAME, expectedParameterValues); + assertThat(actualActionCondition) + .isNotNull().usingRecursiveComparison().ignoringFields("id", "parameterValues.property.prefix") + .isEqualTo(expectedActionCondition); + } + + @Test + public void testToServiceModel_compareMimetype() + { + final SimpleCondition simpleCondition = createSimpleCondition(PARAM_MIMETYPE); + + // when + final ActionCondition actualActionCondition = objectUnderTest.toServiceModel(simpleCondition); + + final Map expectedParameterValues = new HashMap<>(); + expectedParameterValues.put(ComparePropertyValueEvaluator.PARAM_PROPERTY, ContentModel.TYPE_CONTENT); + expectedParameterValues.put(ComparePropertyValueEvaluator.PARAM_VALUE, PARAMETER_DEFAULT); + final ActionCondition expectedActionCondition = new ActionConditionImpl(null, CompareMimeTypeEvaluator.NAME, expectedParameterValues); + assertThat(actualActionCondition) + .isNotNull().usingRecursiveComparison().ignoringFields("id") + .isEqualTo(expectedActionCondition); + } + + @Test + public void testToServiceModel_hasAspect() + { + final QName audioAspect = QName.createQName(NamespaceService.AUDIO_MODEL_1_0_URI, NamespaceService.AUDIO_MODEL_PREFIX); + final String field = NamespaceService.AUDIO_MODEL_PREFIX + QName.NAMESPACE_PREFIX + NamespaceService.AUDIO_MODEL_PREFIX; + final SimpleCondition simpleCondition = createSimpleCondition(HasAspectEvaluator.PARAM_ASPECT, field); + + // when + final ActionCondition actualActionCondition = objectUnderTest.toServiceModel(simpleCondition); + + final Map expectedParameterValues = new HashMap<>(); + expectedParameterValues.put(HasAspectEvaluator.PARAM_ASPECT, audioAspect); + final ActionCondition expectedActionCondition = new ActionConditionImpl(null, HasAspectEvaluator.NAME, expectedParameterValues); + assertThat(actualActionCondition) + .isNotNull().usingRecursiveComparison().ignoringFields("id", "parameterValues.aspect.prefix") + .isEqualTo(expectedActionCondition); + } + + @Test + public void testToServiceModel_hasTag() + { + final String tag = "some tag"; + final SimpleCondition simpleCondition = createSimpleCondition(HasTagEvaluator.PARAM_TAG, tag); + + // when + final ActionCondition actualActionCondition = objectUnderTest.toServiceModel(simpleCondition); + + final Map expectedParameterValues = new HashMap<>(); + expectedParameterValues.put(HasTagEvaluator.PARAM_TAG, tag); + final ActionCondition expectedActionCondition = new ActionConditionImpl(null, HasTagEvaluator.NAME, expectedParameterValues); + assertThat(actualActionCondition) + .isNotNull().usingRecursiveComparison().ignoringFields("id") + .isEqualTo(expectedActionCondition); + } + + @Test + public void testToServiceModel_inCategory() + { + final SimpleCondition simpleCondition = createSimpleCondition(PARAM_CATEGORY); + final NodeRef defaultNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARAMETER_DEFAULT); + given(nodesMock.validateOrLookupNode(PARAMETER_DEFAULT, null)).willReturn(defaultNodeRef); + + // when + final ActionCondition actualActionCondition = objectUnderTest.toServiceModel(simpleCondition); + + final Map expectedParameterValues = new HashMap<>(); + expectedParameterValues.put(InCategoryEvaluator.PARAM_CATEGORY_ASPECT, ContentModel.ASPECT_GEN_CLASSIFIABLE); + expectedParameterValues.put(InCategoryEvaluator.PARAM_CATEGORY_VALUE, defaultNodeRef); + final ActionCondition expectedActionCondition = new ActionConditionImpl(null, InCategoryEvaluator.NAME, expectedParameterValues); + assertThat(actualActionCondition) + .isNotNull().usingRecursiveComparison().ignoringFields("id") + .isEqualTo(expectedActionCondition); + } + + @Test + public void testToServiceModel_isSubType() + { + final String field = NamespaceService.CONTENT_MODEL_PREFIX + QName.NAMESPACE_PREFIX + ContentModel.TYPE_FOLDER.toPrefixString(); + final SimpleCondition simpleCondition = createSimpleCondition(IsSubTypeEvaluator.PARAM_TYPE, field); + + // when + final ActionCondition actualActionCondition = objectUnderTest.toServiceModel(simpleCondition); + + final Map expectedParameterValues = new HashMap<>(); + expectedParameterValues.put(IsSubTypeEvaluator.PARAM_TYPE, ContentModel.TYPE_FOLDER); + final ActionCondition expectedActionCondition = new ActionConditionImpl(null, IsSubTypeEvaluator.NAME, expectedParameterValues); + assertThat(actualActionCondition) + .isNotNull().usingRecursiveComparison().ignoringFields("id", "parameterValues.type.prefix") + .isEqualTo(expectedActionCondition); + } + + @Test + public void testToServiceModel_nullOrBlankParameter() + { + final SimpleCondition simpleConditionNullParam = createSimpleCondition(IsSubTypeEvaluator.PARAM_TYPE, null); + + // when + assertThatThrownBy(() -> objectUnderTest.toServiceModel(simpleConditionNullParam)) + .isInstanceOf(InvalidArgumentException.class) + .hasMessageContaining(PARAMETER_NOT_NULL); + + final SimpleCondition simpleConditionEmptyParam = createSimpleCondition(IsSubTypeEvaluator.PARAM_TYPE, " "); + + assertThatThrownBy(() -> objectUnderTest.toServiceModel(simpleConditionEmptyParam)) + .isInstanceOf(InvalidArgumentException.class) + .hasMessageContaining(PARAMETER_NOT_NULL); + } + + @Test + public void testToServiceModel_nullOrEmptyField() + { + final SimpleCondition simpleConditionNullField = createSimpleCondition(null); + + // when + assertThatThrownBy(() -> objectUnderTest.toServiceModel(simpleConditionNullField)) + .isInstanceOf(InvalidArgumentException.class) + .hasMessageContaining(FIELD_NOT_NULL); + + final SimpleCondition simpleConditionEmptyField = createSimpleCondition(""); + + // when + assertThatThrownBy(() -> objectUnderTest.toServiceModel(simpleConditionEmptyField)) + .isInstanceOf(InvalidArgumentException.class) + .hasMessageContaining(FIELD_NOT_NULL); + } + + @Test + public void testToServiceModel_nullOrEmptyComparatorWhenRequired() + { + final SimpleCondition simpleConditionNullComparator = SimpleCondition.builder() + .field("size") + .comparator(null) + .parameter("65000") + .create(); + + // when + assertThatThrownBy(() -> objectUnderTest.toServiceModel(simpleConditionNullComparator)) + .isInstanceOf(InvalidArgumentException.class) + .hasMessageContaining(COMPARATOR_NOT_NULL); + + final SimpleCondition simpleConditionEmptyComparator = SimpleCondition.builder() + .field("size") + .comparator(" ") + .parameter("65000") + .create(); + + // when + assertThatThrownBy(() -> objectUnderTest.toServiceModel(simpleConditionEmptyComparator)) + .isInstanceOf(InvalidArgumentException.class) + .hasMessageContaining(COMPARATOR_NOT_NULL); + } + + private static ActionCondition createActionCondition(final String actionDefinitionName) + { + return new ActionConditionImpl("fake-id", actionDefinitionName, createParameterValues()); + } + + private static Map createParameterValues() { + final QName audioAspect = QName.createQName(NamespaceService.AUDIO_MODEL_1_0_URI, NamespaceService.AUDIO_MODEL_PREFIX); + final NodeRef defaultNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARAMETER_DEFAULT); + final Map parameterValues = new HashMap<>(); + parameterValues.put(ComparePropertyValueEvaluator.PARAM_CONTENT_PROPERTY, "content-property"); + parameterValues.put(ComparePropertyValueEvaluator.PARAM_PROPERTY, ContentModel.TYPE_CONTENT); + parameterValues.put(ComparePropertyValueEvaluator.PARAM_OPERATION, "operation"); + parameterValues.put(ComparePropertyValueEvaluator.PARAM_VALUE, "value"); + parameterValues.put(HasAspectEvaluator.PARAM_ASPECT, audioAspect); + parameterValues.put(HasTagEvaluator.PARAM_TAG, "tag"); + parameterValues.put(InCategoryEvaluator.PARAM_CATEGORY_ASPECT, "category-aspect"); + parameterValues.put(InCategoryEvaluator.PARAM_CATEGORY_VALUE, defaultNodeRef); + parameterValues.put(IsSubTypeEvaluator.PARAM_TYPE, ContentModel.TYPE_FOLDER); + + return parameterValues; + } + + private static SimpleCondition createSimpleCondition(final String field) + { + return createSimpleCondition(field, PARAMETER_DEFAULT); + } + + private static SimpleCondition createSimpleCondition(final String field, final String parameter) + { + return SimpleCondition.builder() + .field(field) + .comparator(ComparePropertyValueOperation.EQUALS.toString().toLowerCase()) + .parameter(parameter) + .create(); + } + + private static List getTestData() { + return List.of( + TestData.of(ComparePropertyValueEvaluator.NAME), + TestData.of(CompareMimeTypeEvaluator.NAME), + TestData.of(HasAspectEvaluator.NAME), + TestData.of(HasChildEvaluator.NAME, NULL_RESULT), + TestData.of(HasTagEvaluator.NAME), + TestData.of(HasVersionHistoryEvaluator.NAME, NULL_RESULT), + TestData.of(InCategoryEvaluator.NAME), + TestData.of(IsSubTypeEvaluator.NAME), + TestData.of(NoConditionEvaluator.NAME, NULL_RESULT), + TestData.of("fake-definition-name", NULL_RESULT), + TestData.of("", NULL_RESULT), + TestData.of(null, NULL_RESULT) + ); + } + + private static class TestData + { + String conditionDefinitionName; + boolean isNullResult; + + public TestData(String conditionDefinitionName, boolean isNullResult) + { + this.conditionDefinitionName = conditionDefinitionName; + this.isNullResult = isNullResult; + } + + public static TestData of(String conditionDefinitionName) { + return new TestData(conditionDefinitionName, false); + } + + public static TestData of(String conditionDefinitionName, boolean isNullResult) { + return new TestData(conditionDefinitionName, isNullResult); + } + } +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/ActionParameterConverterTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/ActionParameterConverterTest.java new file mode 100644 index 0000000000..98d24839e8 --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/ActionParameterConverterTest.java @@ -0,0 +1,686 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.rules; + +import static org.alfresco.repo.action.executer.CopyActionExecuter.PARAM_DESTINATION_FOLDER; +import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE; +import static org.alfresco.service.cmr.security.AccessStatus.ALLOWED; +import static org.alfresco.service.cmr.security.AccessStatus.DENIED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.alfresco.repo.action.executer.AddFeaturesActionExecuter; +import org.alfresco.repo.action.executer.CheckInActionExecuter; +import org.alfresco.repo.action.executer.CheckOutActionExecuter; +import org.alfresco.repo.action.executer.CopyActionExecuter; +import org.alfresco.repo.action.executer.LinkCategoryActionExecuter; +import org.alfresco.repo.action.executer.MoveActionExecuter; +import org.alfresco.repo.action.executer.RemoveFeaturesActionExecuter; +import org.alfresco.repo.action.executer.ScriptActionExecuter; +import org.alfresco.repo.action.executer.SetPropertyValueActionExecuter; +import org.alfresco.repo.action.executer.SimpleWorkflowActionExecuter; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.action.ActionDefinition; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.action.ParameterDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.logging.log4j.util.Strings; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@Experimental +@RunWith(MockitoJUnitRunner.class) +public class ActionParameterConverterTest +{ + private static final String VERSIONABLE = "versionable"; + private static final String VERSIONABLE_ASPECT = NamespaceService.CONTENT_MODEL_PREFIX + QName.NAMESPACE_PREFIX + VERSIONABLE; + private static final String CHECKOUT = "checkout"; + private static final String CHECKOUT_ASPECT = NamespaceService.CONTENT_MODEL_PREFIX + QName.NAMESPACE_PREFIX + CHECKOUT; + private static final String CONTAINS = "contains"; + private static final String CONTAINS_ASPECT = NamespaceService.CONTENT_MODEL_PREFIX + QName.NAMESPACE_PREFIX + CONTAINS; + private static final String CLASSIFIABLE = "generalclassifiable"; + private static final String CLASSIFIABLE_ASPECT = NamespaceService.CONTENT_MODEL_PREFIX + QName.NAMESPACE_PREFIX + CLASSIFIABLE; + private static final String IDENTIFIER = "identifier"; + private static final String IDENTIFIER_ASPECT = NamespaceService.CONTENT_MODEL_PREFIX + QName.NAMESPACE_PREFIX + IDENTIFIER; + + private static final String DUMMY_FOLDER_NODE_ID = "dummy://folder/node"; + private static final NodeRef DUMMY_FOLDER_NODE = new NodeRef(DUMMY_FOLDER_NODE_ID); + private static final String DUMMY_SCRIPT_NODE_ID = "dummy://script/ref"; + private static final NodeRef DUMMY_SCRIPT_NODE = new NodeRef(DUMMY_SCRIPT_NODE_ID); + + @Mock + private DictionaryService dictionaryService; + @Mock + private ActionService actionService; + @Mock + private NamespaceService namespaceService; + @Mock + private PermissionService permissionService; + @Mock + private Nodes nodes; + + @Mock + private ActionDefinition actionDefinition; + @Mock + private ParameterDefinition actionDefinitionParam1; + @Mock + private ParameterDefinition actionDefinitionParam2; + @Mock + private ParameterDefinition actionDefinitionParam3; + @Mock + private DataTypeDefinition dataTypeDefinition1; + @Mock + private DataTypeDefinition dataTypeDefinition2; + @Mock + private DataTypeDefinition dataTypeDefinition3; + + @InjectMocks + private ActionParameterConverter objectUnderTest; + + @Before + public void setUp() + { + given(nodes.validateOrLookupNode(DUMMY_FOLDER_NODE_ID, null)).willReturn(DUMMY_FOLDER_NODE); + given(nodes.validateOrLookupNode(DUMMY_SCRIPT_NODE_ID, null)).willReturn(DUMMY_SCRIPT_NODE); + given(permissionService.hasReadPermission(DUMMY_FOLDER_NODE)).willReturn(ALLOWED); + given(permissionService.hasReadPermission(DUMMY_SCRIPT_NODE)).willReturn(ALLOWED); + } + + @Test + public void testAddAspectConversion() + { + final String name = AddFeaturesActionExecuter.NAME; + final String aspectNameKey = AddFeaturesActionExecuter.PARAM_ASPECT_NAME; + final Map params = Map.of(aspectNameKey, VERSIONABLE_ASPECT); + + given(actionService.getActionDefinition(name)).willReturn(actionDefinition); + given(actionDefinition.getParameterDefintion(aspectNameKey)).willReturn(actionDefinitionParam1); + final QName qname = DataTypeDefinition.QNAME; + given(actionDefinitionParam1.getType()).willReturn(qname); + given(dictionaryService.getDataType(qname)).willReturn(dataTypeDefinition1); + given(namespaceService.getNamespaceURI(any())).willReturn(NamespaceService.DICTIONARY_MODEL_1_0_URI); + + //when + final Map convertedParams = objectUnderTest.getConvertedParams(params, name); + + then(actionService).should().getActionDefinition(name); + then(actionService).shouldHaveNoMoreInteractions(); + then(actionDefinition).should().getParameterDefintion(aspectNameKey); + then(actionDefinition).shouldHaveNoMoreInteractions(); + then(dictionaryService).should().getDataType(qname); + then(dictionaryService).shouldHaveNoMoreInteractions(); + then(namespaceService).should().getNamespaceURI(any()); + then(namespaceService).shouldHaveNoMoreInteractions(); + + final Serializable convertedParam = convertedParams.get(aspectNameKey); + assertTrue(convertedParam instanceof QName); + assertEquals(VERSIONABLE, ((QName) convertedParam).getLocalName()); + assertEquals(VERSIONABLE_ASPECT, ((QName) convertedParam).getPrefixString()); + assertEquals(NamespaceService.DICTIONARY_MODEL_1_0_URI, ((QName) convertedParam).getNamespaceURI()); + } + + @Test + public void testCopyConversion() + { + final String name = CopyActionExecuter.NAME; + final String destinationFolderKey = PARAM_DESTINATION_FOLDER; + final String deepCopyKey = CopyActionExecuter.PARAM_DEEP_COPY; + final Map params = Map.of(destinationFolderKey, DUMMY_FOLDER_NODE_ID, deepCopyKey, true); + + given(actionService.getActionDefinition(name)).willReturn(actionDefinition); + given(actionDefinition.getParameterDefintion(destinationFolderKey)).willReturn(actionDefinitionParam1); + given(actionDefinition.getParameterDefintion(deepCopyKey)).willReturn(actionDefinitionParam2); + final QName nodeRef = DataTypeDefinition.NODE_REF; + given(actionDefinitionParam1.getType()).willReturn(nodeRef); + final QName bool = DataTypeDefinition.BOOLEAN; + given(actionDefinitionParam2.getType()).willReturn(bool); + + given(dictionaryService.getDataType(nodeRef)).willReturn(dataTypeDefinition1); + given(dictionaryService.getDataType(bool)).willReturn(dataTypeDefinition2); + given(dataTypeDefinition2.getJavaClassName()).willReturn(Boolean.class.getName()); + + //when + final Map convertedParams = objectUnderTest.getConvertedParams(params, name); + + then(actionService).should().getActionDefinition(name); + then(actionService).shouldHaveNoMoreInteractions(); + then(actionDefinition).should().getParameterDefintion(destinationFolderKey); + then(actionDefinition).should().getParameterDefintion(deepCopyKey); + then(actionDefinition).shouldHaveNoMoreInteractions(); + then(dictionaryService).should(times(2)).getDataType(bool); + then(dictionaryService).should().getDataType(nodeRef); + then(dictionaryService).shouldHaveNoMoreInteractions(); + then(namespaceService).shouldHaveNoInteractions(); + + final Serializable convertedCopyParam = convertedParams.get(destinationFolderKey); + assertTrue(convertedCopyParam instanceof NodeRef); + assertEquals(DUMMY_FOLDER_NODE, convertedCopyParam); + final Serializable convertedDeepCopyParam = convertedParams.get(deepCopyKey); + assertThat(convertedDeepCopyParam instanceof Boolean).isTrue(); + assertTrue(((Boolean) convertedDeepCopyParam)); + } + + @Test + public void testExecuteScriptConversion() + { + final String name = ScriptActionExecuter.NAME; + final String executeScriptKey = ScriptActionExecuter.PARAM_SCRIPTREF; + final Map params = Map.of(executeScriptKey, DUMMY_SCRIPT_NODE_ID); + + given(actionService.getActionDefinition(name)).willReturn(actionDefinition); + given(actionDefinition.getParameterDefintion(executeScriptKey)).willReturn(actionDefinitionParam1); + final QName scriptNodeRef = DataTypeDefinition.NODE_REF; + given(actionDefinitionParam1.getType()).willReturn(scriptNodeRef); + + given(dictionaryService.getDataType(scriptNodeRef)).willReturn(dataTypeDefinition1); + + //when + final Map convertedParams = objectUnderTest.getConvertedParams(params, name); + + then(actionService).should().getActionDefinition(name); + then(actionService).shouldHaveNoMoreInteractions(); + then(actionDefinition).should().getParameterDefintion(executeScriptKey); + then(actionDefinition).shouldHaveNoMoreInteractions(); + then(dictionaryService).should().getDataType(scriptNodeRef); + then(dictionaryService).shouldHaveNoMoreInteractions(); + then(namespaceService).shouldHaveNoInteractions(); + + final Serializable convertedCopyParam = convertedParams.get(executeScriptKey); + assertTrue(convertedCopyParam instanceof NodeRef); + assertEquals(DUMMY_SCRIPT_NODE, convertedCopyParam); + } + + @Test + public void testMoveConversion() + { + final String name = MoveActionExecuter.NAME; + final String destinationFolderKey = MoveActionExecuter.PARAM_DESTINATION_FOLDER; + final Map params = Map.of(destinationFolderKey, DUMMY_FOLDER_NODE_ID); + + given(actionService.getActionDefinition(name)).willReturn(actionDefinition); + given(actionDefinition.getParameterDefintion(destinationFolderKey)).willReturn(actionDefinitionParam1); + final QName nodeRef = DataTypeDefinition.NODE_REF; + given(actionDefinitionParam1.getType()).willReturn(nodeRef); + + given(dictionaryService.getDataType(nodeRef)).willReturn(dataTypeDefinition1); + + //when + final Map convertedParams = objectUnderTest.getConvertedParams(params, name); + + then(actionService).should().getActionDefinition(name); + then(actionService).shouldHaveNoMoreInteractions(); + then(actionDefinition).should().getParameterDefintion(destinationFolderKey); + then(actionDefinition).shouldHaveNoMoreInteractions(); + then(dictionaryService).should().getDataType(nodeRef); + then(dictionaryService).shouldHaveNoMoreInteractions(); + then(namespaceService).shouldHaveNoInteractions(); + + final Serializable convertedCopyParam = convertedParams.get(destinationFolderKey); + assertTrue(convertedCopyParam instanceof NodeRef); + assertEquals(DUMMY_FOLDER_NODE, convertedCopyParam); + } + + @Test + public void testCheckInConversion() + { + final String name = CheckInActionExecuter.NAME; + final String descriptionKey = CheckInActionExecuter.PARAM_DESCRIPTION; + final String minorChangeKey = CheckInActionExecuter.PARAM_MINOR_CHANGE; + String description = "dummy description"; + final Map params = Map.of(descriptionKey, description, minorChangeKey, true); + + given(actionService.getActionDefinition(name)).willReturn(actionDefinition); + given(actionDefinition.getParameterDefintion(descriptionKey)).willReturn(actionDefinitionParam1); + given(actionDefinition.getParameterDefintion(minorChangeKey)).willReturn(actionDefinitionParam2); + final QName text = DataTypeDefinition.TEXT; + given(actionDefinitionParam1.getType()).willReturn(text); + final QName bool = DataTypeDefinition.BOOLEAN; + given(actionDefinitionParam2.getType()).willReturn(bool); + + given(dictionaryService.getDataType(text)).willReturn(dataTypeDefinition1); + given(dataTypeDefinition1.getJavaClassName()).willReturn(String.class.getName()); + given(dictionaryService.getDataType(bool)).willReturn(dataTypeDefinition2); + given(dataTypeDefinition2.getJavaClassName()).willReturn(Boolean.class.getName()); + + //when + final Map convertedParams = objectUnderTest.getConvertedParams(params, name); + + then(actionService).should().getActionDefinition(name); + then(actionService).shouldHaveNoMoreInteractions(); + then(actionDefinition).should().getParameterDefintion(descriptionKey); + then(actionDefinition).should().getParameterDefintion(minorChangeKey); + then(actionDefinition).shouldHaveNoMoreInteractions(); + then(dictionaryService).should(times(2)).getDataType(bool); + then(dictionaryService).should(times(2)).getDataType(text); + then(dictionaryService).shouldHaveNoMoreInteractions(); + then(namespaceService).shouldHaveNoInteractions(); + + final Serializable convertedDescriptionParam = convertedParams.get(descriptionKey); + assertTrue(convertedDescriptionParam instanceof String); + assertEquals(description, convertedDescriptionParam); + final Serializable convertedMinorChangeParam = convertedParams.get(minorChangeKey); + assertTrue(convertedMinorChangeParam instanceof Boolean); + assertTrue((Boolean) convertedMinorChangeParam); + } + + @Test + public void testCheckOutConversion() + { + final String name = CheckOutActionExecuter.NAME; + final String destinationFolderKey = CheckOutActionExecuter.PARAM_DESTINATION_FOLDER; + final String assocNameKey = CheckOutActionExecuter.PARAM_ASSOC_QNAME; + final String assocTypeKey = CheckOutActionExecuter.PARAM_ASSOC_TYPE_QNAME; + final Map params = + Map.of(destinationFolderKey, DUMMY_FOLDER_NODE_ID, assocNameKey, CHECKOUT_ASPECT, assocTypeKey, CONTAINS_ASPECT); + + given(actionService.getActionDefinition(name)).willReturn(actionDefinition); + given(actionDefinition.getParameterDefintion(destinationFolderKey)).willReturn(actionDefinitionParam1); + final QName nodeRef = DataTypeDefinition.NODE_REF; + given(actionDefinitionParam1.getType()).willReturn(nodeRef); + given(actionDefinition.getParameterDefintion(assocNameKey)).willReturn(actionDefinitionParam2); + final QName qname = DataTypeDefinition.QNAME; + given(actionDefinitionParam2.getType()).willReturn(qname); + given(actionDefinition.getParameterDefintion(assocTypeKey)).willReturn(actionDefinitionParam3); + given(actionDefinitionParam3.getType()).willReturn(qname); + + given(dictionaryService.getDataType(nodeRef)).willReturn(dataTypeDefinition1); + given(dictionaryService.getDataType(qname)).willReturn(dataTypeDefinition2); + given(namespaceService.getNamespaceURI(any())).willReturn(NamespaceService.DICTIONARY_MODEL_1_0_URI); + + //when + final Map convertedParams = objectUnderTest.getConvertedParams(params, name); + + then(actionService).should().getActionDefinition(name); + then(actionService).shouldHaveNoMoreInteractions(); + then(actionDefinition).should().getParameterDefintion(destinationFolderKey); + then(actionDefinition).should().getParameterDefintion(assocNameKey); + then(actionDefinition).should().getParameterDefintion(assocTypeKey); + then(actionDefinition).shouldHaveNoMoreInteractions(); + then(dictionaryService).should(times(2)).getDataType(qname); + then(dictionaryService).should().getDataType(nodeRef); + then(dictionaryService).shouldHaveNoMoreInteractions(); + then(namespaceService).should(times(2)).getNamespaceURI(any()); + then(namespaceService).shouldHaveNoMoreInteractions(); + + final Serializable convertedDestinationParam = convertedParams.get(destinationFolderKey); + assertTrue(convertedDestinationParam instanceof NodeRef); + assertEquals(DUMMY_FOLDER_NODE, convertedDestinationParam); + final Serializable convertedAssocNameParam = convertedParams.get(assocNameKey); + assertTrue(convertedAssocNameParam instanceof QName); + assertEquals(CHECKOUT, ((QName) convertedAssocNameParam).getLocalName()); + assertEquals(CHECKOUT_ASPECT, ((QName) convertedAssocNameParam).getPrefixString()); + assertEquals(NamespaceService.DICTIONARY_MODEL_1_0_URI, ((QName) convertedAssocNameParam).getNamespaceURI()); + final Serializable convertedAssocTypeParam = convertedParams.get(assocTypeKey); + assertTrue(convertedAssocTypeParam instanceof QName); + assertEquals(CONTAINS, ((QName) convertedAssocTypeParam).getLocalName()); + assertEquals(CONTAINS_ASPECT, ((QName) convertedAssocTypeParam).getPrefixString()); + assertEquals(NamespaceService.DICTIONARY_MODEL_1_0_URI, ((QName) convertedAssocTypeParam).getNamespaceURI()); + } + + @Test + public void testCategoryLinkConversion() + { + final String name = LinkCategoryActionExecuter.NAME; + final String categoryAspectKey = LinkCategoryActionExecuter.PARAM_CATEGORY_ASPECT; + final String categoryValueKey = LinkCategoryActionExecuter.PARAM_CATEGORY_VALUE; + final Map params = Map.of(categoryAspectKey, CLASSIFIABLE_ASPECT, categoryValueKey, DUMMY_FOLDER_NODE_ID); + + given(actionService.getActionDefinition(name)).willReturn(actionDefinition); + given(actionDefinition.getParameterDefintion(categoryAspectKey)).willReturn(actionDefinitionParam1); + final QName qname = DataTypeDefinition.QNAME; + given(actionDefinitionParam1.getType()).willReturn(qname); + given(actionDefinition.getParameterDefintion(categoryValueKey)).willReturn(actionDefinitionParam2); + final QName nodeRef = DataTypeDefinition.NODE_REF; + given(actionDefinitionParam2.getType()).willReturn(nodeRef); + + given(dictionaryService.getDataType(nodeRef)).willReturn(dataTypeDefinition1); + given(dictionaryService.getDataType(qname)).willReturn(dataTypeDefinition2); + given(namespaceService.getNamespaceURI(any())).willReturn(NamespaceService.DICTIONARY_MODEL_1_0_URI); + + //when + final Map convertedParams = objectUnderTest.getConvertedParams(params, name); + + then(actionService).should().getActionDefinition(name); + then(actionService).shouldHaveNoMoreInteractions(); + then(actionDefinition).should().getParameterDefintion(categoryAspectKey); + then(actionDefinition).should().getParameterDefintion(categoryValueKey); + then(actionDefinition).shouldHaveNoMoreInteractions(); + then(dictionaryService).should().getDataType(qname); + then(dictionaryService).should().getDataType(nodeRef); + then(dictionaryService).shouldHaveNoMoreInteractions(); + then(namespaceService).should().getNamespaceURI(any()); + then(namespaceService).shouldHaveNoMoreInteractions(); + + final Serializable convertedCatValueParam = convertedParams.get(categoryAspectKey); + assertTrue(convertedCatValueParam instanceof QName); + assertEquals(CLASSIFIABLE, ((QName) convertedCatValueParam).getLocalName()); + assertEquals(CLASSIFIABLE_ASPECT, ((QName) convertedCatValueParam).getPrefixString()); + assertEquals(NamespaceService.DICTIONARY_MODEL_1_0_URI, ((QName) convertedCatValueParam).getNamespaceURI()); + final Serializable convertedDestinationParam = convertedParams.get(categoryValueKey); + assertTrue(convertedDestinationParam instanceof NodeRef); + assertEquals(DUMMY_FOLDER_NODE, convertedDestinationParam); + } + + @Test + public void testRemoveAspectConversion() + { + final String name = RemoveFeaturesActionExecuter.NAME; + final String aspectNameKey = RemoveFeaturesActionExecuter.PARAM_ASPECT_NAME; + final Map params = Map.of(aspectNameKey, VERSIONABLE_ASPECT); + + given(actionService.getActionDefinition(name)).willReturn(actionDefinition); + given(actionDefinition.getParameterDefintion(aspectNameKey)).willReturn(actionDefinitionParam1); + final QName qname = DataTypeDefinition.QNAME; + given(actionDefinitionParam1.getType()).willReturn(qname); + given(dictionaryService.getDataType(qname)).willReturn(dataTypeDefinition1); + given(namespaceService.getNamespaceURI(any())).willReturn(NamespaceService.DICTIONARY_MODEL_1_0_URI); + + //when + final Map convertedParams = objectUnderTest.getConvertedParams(params, name); + + then(actionService).should().getActionDefinition(name); + then(actionService).shouldHaveNoMoreInteractions(); + then(actionDefinition).should().getParameterDefintion(aspectNameKey); + then(actionDefinition).shouldHaveNoMoreInteractions(); + then(dictionaryService).should().getDataType(qname); + then(dictionaryService).shouldHaveNoMoreInteractions(); + then(namespaceService).should().getNamespaceURI(any()); + then(namespaceService).shouldHaveNoMoreInteractions(); + + final Serializable convertedParam = convertedParams.get(aspectNameKey); + assertTrue(convertedParam instanceof QName); + assertEquals(VERSIONABLE, ((QName) convertedParam).getLocalName()); + assertEquals(VERSIONABLE_ASPECT, ((QName) convertedParam).getPrefixString()); + assertEquals(NamespaceService.DICTIONARY_MODEL_1_0_URI, ((QName) convertedParam).getNamespaceURI()); + } + + @Test + public void testAddWorkflowConversion() + { + final String name = SimpleWorkflowActionExecuter.NAME; + final String approveStepKey = SimpleWorkflowActionExecuter.PARAM_APPROVE_STEP; + final String approveFolderKey = SimpleWorkflowActionExecuter.PARAM_APPROVE_FOLDER; + final String approveMoveKey = SimpleWorkflowActionExecuter.PARAM_APPROVE_MOVE; + final String rejectStepKey = SimpleWorkflowActionExecuter.PARAM_REJECT_STEP; + final String rejectFolderKey = SimpleWorkflowActionExecuter.PARAM_REJECT_FOLDER; + final String rejectMoveKey = SimpleWorkflowActionExecuter.PARAM_REJECT_MOVE; + final String approve = "Approve"; + final String reject = "Reject"; + final Map params = + Map.of(approveStepKey, approve, approveFolderKey, DUMMY_FOLDER_NODE_ID, approveMoveKey, true, + rejectStepKey, reject, rejectFolderKey, DUMMY_FOLDER_NODE_ID, rejectMoveKey, true); + + given(actionService.getActionDefinition(name)).willReturn(actionDefinition); + given(actionDefinition.getParameterDefintion(rejectStepKey)).willReturn(actionDefinitionParam1); + given(actionDefinition.getParameterDefintion(approveStepKey)).willReturn(actionDefinitionParam1); + final QName text = DataTypeDefinition.TEXT; + given(actionDefinitionParam1.getType()).willReturn(text, text); + given(actionDefinition.getParameterDefintion(rejectFolderKey)).willReturn(actionDefinitionParam2); + given(actionDefinition.getParameterDefintion(approveFolderKey)).willReturn(actionDefinitionParam2); + final QName nodeRef = DataTypeDefinition.NODE_REF; + given(actionDefinitionParam2.getType()).willReturn(nodeRef, nodeRef); + given(actionDefinition.getParameterDefintion(rejectMoveKey)).willReturn(actionDefinitionParam3); + given(actionDefinition.getParameterDefintion(approveMoveKey)).willReturn(actionDefinitionParam3); + final QName bool = DataTypeDefinition.BOOLEAN; + given(actionDefinitionParam3.getType()).willReturn(bool, bool); + + given(dictionaryService.getDataType(nodeRef)).willReturn(dataTypeDefinition1); + given(dictionaryService.getDataType(text)).willReturn(dataTypeDefinition2); + given(dataTypeDefinition2.getJavaClassName()).willReturn(String.class.getName()); + given(dictionaryService.getDataType(bool)).willReturn(dataTypeDefinition3); + given(dataTypeDefinition3.getJavaClassName()).willReturn(Boolean.class.getName()); + + //when + final Map convertedParams = objectUnderTest.getConvertedParams(params, name); + + then(actionService).should().getActionDefinition(name); + then(actionService).shouldHaveNoMoreInteractions(); + then(actionDefinition).should().getParameterDefintion(approveStepKey); + then(actionDefinition).should().getParameterDefintion(approveFolderKey); + then(actionDefinition).should().getParameterDefintion(approveMoveKey); + then(actionDefinition).should().getParameterDefintion(rejectStepKey); + then(actionDefinition).should().getParameterDefintion(rejectFolderKey); + then(actionDefinition).should().getParameterDefintion(rejectMoveKey); + then(actionDefinition).shouldHaveNoMoreInteractions(); + then(dictionaryService).should(times(4)).getDataType(text); + then(dictionaryService).should(times(2)).getDataType(nodeRef); + then(dictionaryService).should(times(4)).getDataType(bool); + then(dictionaryService).shouldHaveNoMoreInteractions(); + then(namespaceService).shouldHaveNoInteractions(); + + final Serializable convertedApproveStepParam = convertedParams.get(approveStepKey); + assertTrue(convertedApproveStepParam instanceof String); + assertEquals(approve, convertedApproveStepParam); + final Serializable convertedRejectStepParam = convertedParams.get(rejectStepKey); + assertTrue(convertedRejectStepParam instanceof String); + assertEquals(reject, convertedRejectStepParam); + final Serializable convertedApproveFolderParam = convertedParams.get(approveFolderKey); + assertTrue(convertedApproveFolderParam instanceof NodeRef); + assertEquals(DUMMY_FOLDER_NODE, convertedApproveFolderParam); + final Serializable convertedRejectFolderParam = convertedParams.get(rejectFolderKey); + assertTrue(convertedRejectFolderParam instanceof NodeRef); + assertEquals(DUMMY_FOLDER_NODE, convertedRejectFolderParam); + final Serializable convertedApproveMoveParam = convertedParams.get(approveMoveKey); + assertTrue(convertedApproveMoveParam instanceof Boolean); + assertTrue((Boolean) convertedApproveMoveParam); + final Serializable convertedRejectMoveParam = convertedParams.get(rejectMoveKey); + assertTrue(convertedRejectMoveParam instanceof Boolean); + assertTrue((Boolean) convertedRejectMoveParam); + } + + @Test + public void testSetPropertyConversion() + { + final String name = SetPropertyValueActionExecuter.NAME; + final String propertyNameKey = SetPropertyValueActionExecuter.PARAM_PROPERTY; + final String propertyValueKey = SetPropertyValueActionExecuter.PARAM_VALUE; + final String propertyTypeKey = "prop_type"; + final String dummy_key_value = "dummy_key_value"; + final String propType = "d:text"; + final Map params = + Map.of(propertyNameKey, IDENTIFIER_ASPECT, propertyValueKey, dummy_key_value, propertyTypeKey, propType); + + given(actionService.getActionDefinition(name)).willReturn(actionDefinition); + given(actionDefinition.getParameterDefintion(propertyNameKey)).willReturn(actionDefinitionParam1); + final QName qname = DataTypeDefinition.QNAME; + given(actionDefinitionParam1.getType()).willReturn(qname); + given(actionDefinition.getParameterDefintion(propertyValueKey)).willReturn(actionDefinitionParam2); + final QName any = DataTypeDefinition.ANY; + given(actionDefinitionParam2.getType()).willReturn(any); + given(actionDefinition.getParameterDefintion(propertyTypeKey)).willReturn(null); + given(actionDefinition.getAdhocPropertiesAllowed()).willReturn(true); + + given(dictionaryService.getDataType(qname)).willReturn(dataTypeDefinition1); + given(dictionaryService.getDataType(any)).willReturn(dataTypeDefinition2); + given(dataTypeDefinition2.getJavaClassName()).willReturn(Object.class.getName()); + given(namespaceService.getNamespaceURI(any())).willReturn(NamespaceService.DICTIONARY_MODEL_1_0_URI); + + //when + final Map convertedParams = objectUnderTest.getConvertedParams(params, name); + + then(actionService).should().getActionDefinition(name); + then(actionService).shouldHaveNoMoreInteractions(); + then(actionDefinition).should().getParameterDefintion(propertyNameKey); + then(actionDefinition).should().getParameterDefintion(propertyValueKey); + then(actionDefinition).should().getParameterDefintion(propertyTypeKey); + then(actionDefinition).should().getAdhocPropertiesAllowed(); + then(actionDefinition).shouldHaveNoMoreInteractions(); + then(dictionaryService).should().getDataType(qname); + then(dictionaryService).should(times(2)).getDataType(any); + then(dictionaryService).shouldHaveNoMoreInteractions(); + then(namespaceService).should().getNamespaceURI(any()); + then(namespaceService).shouldHaveNoMoreInteractions(); + + final Serializable convertedPropNameParam = convertedParams.get(propertyNameKey); + assertTrue(convertedPropNameParam instanceof QName); + assertEquals(IDENTIFIER, ((QName) convertedPropNameParam).getLocalName()); + assertEquals(IDENTIFIER_ASPECT, ((QName) convertedPropNameParam).getPrefixString()); + assertEquals(NamespaceService.DICTIONARY_MODEL_1_0_URI, ((QName) convertedPropNameParam).getNamespaceURI()); + + final Serializable convertedPropValParam = convertedParams.get(propertyValueKey); + assertTrue(convertedPropValParam instanceof String); + assertEquals(dummy_key_value, convertedPropValParam); + final Serializable convertedPropTypeParam = convertedParams.get(propertyTypeKey); + assertTrue(convertedPropTypeParam instanceof String); + assertEquals(propType, convertedPropTypeParam); + } + + @Test + public void testNonExistentNodeParam() + { + final String name = CopyActionExecuter.NAME; + final Map params = Map.of(PARAM_DESTINATION_FOLDER, "non://existent/node"); + + given(actionService.getActionDefinition(name)).willReturn(actionDefinition); + given(actionDefinition.getParameterDefintion(PARAM_DESTINATION_FOLDER)).willReturn(actionDefinitionParam1); + final QName nodeRef = DataTypeDefinition.NODE_REF; + given(actionDefinitionParam1.getType()).willReturn(nodeRef); + + given(dictionaryService.getDataType(nodeRef)).willReturn(dataTypeDefinition1); + + //when + assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> objectUnderTest.getConvertedParams(params, name)); + } + + @Test + public void testNoReadPermissionForNodeParam() + { + final String name = CopyActionExecuter.NAME; + String permissionDeniedNodeId = "permission://denied/node"; + final Map params = Map.of(PARAM_DESTINATION_FOLDER, permissionDeniedNodeId); + NodeRef permissionDeniedNode = new NodeRef(permissionDeniedNodeId); + given(nodes.validateOrLookupNode(permissionDeniedNodeId, null)).willReturn(permissionDeniedNode); + given(permissionService.hasReadPermission(permissionDeniedNode)).willReturn(DENIED); + + given(actionService.getActionDefinition(name)).willReturn(actionDefinition); + given(actionDefinition.getParameterDefintion(PARAM_DESTINATION_FOLDER)).willReturn(actionDefinitionParam1); + final QName nodeRef = DataTypeDefinition.NODE_REF; + given(actionDefinitionParam1.getType()).willReturn(nodeRef); + + given(dictionaryService.getDataType(nodeRef)).willReturn(dataTypeDefinition1); + + //when + assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> objectUnderTest.getConvertedParams(params, name)); + } + + @Test + public void testNullParamValue() + { + final String name = CheckOutActionExecuter.NAME; + final String destinationFolderKey = CheckOutActionExecuter.PARAM_DESTINATION_FOLDER; + final String assocNameKey = CheckOutActionExecuter.PARAM_ASSOC_QNAME; + final String assocTypeKey = CheckOutActionExecuter.PARAM_ASSOC_TYPE_QNAME; + final Map params = + Map.of(destinationFolderKey, Strings.EMPTY, assocNameKey, Strings.EMPTY, assocTypeKey, Strings.EMPTY); + + given(actionService.getActionDefinition(name)).willReturn(actionDefinition); + + //when + assertThrows(InvalidArgumentException.class, () ->objectUnderTest.getConvertedParams(params, name)); + + then(actionService).should().getActionDefinition(name); + then(actionService).shouldHaveNoMoreInteractions(); + then(actionDefinition).shouldHaveNoInteractions(); + then(dictionaryService).shouldHaveNoInteractions(); + then(namespaceService).shouldHaveNoInteractions(); + } + + @Test + public void testInvalidActionDefinitionConversion() { + final String invalidName = "dummy-definition"; + final Map params = Map.of("dummy-key", "dummy-value"); + given(actionService.getActionDefinition(invalidName)).willReturn(null); + //when-then + assertThrows(NotFoundException.class, () ->objectUnderTest.getConvertedParams(params, invalidName)); + + given(actionService.getActionDefinition(invalidName)).willThrow(NotFoundException.class); + //when-then + assertThrows(NotFoundException.class, () ->objectUnderTest.getConvertedParams(params, invalidName)); + } + + @Test + public void testQnameServiceModelParamConversion() { + given(namespaceService.getPrefixes(any())).willReturn(List.of("cm")); + final String qname = "{cm-dummy-prefix}audio"; + final Serializable qnameParam = QName.createQName(qname); + + //when + final Serializable convertedParam = objectUnderTest.convertParamFromServiceModel(qnameParam); + + then(namespaceService).should().getPrefixes(any()); + then(namespaceService).shouldHaveNoMoreInteractions(); + assertEquals("cm:audio", convertedParam); + } + + @Test + public void testNodeRefServiceModelParamConversion() { + //given + final Serializable nodeRefParam = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, DUMMY_FOLDER_NODE_ID); + + //when + final Serializable convertedParam = objectUnderTest.convertParamFromServiceModel(nodeRefParam); + + then(namespaceService).shouldHaveNoInteractions(); + assertEquals(DUMMY_FOLDER_NODE_ID, convertedParam); + } + + @Test + public void testOtherServiceModelParamConversion() { + //given + final Serializable dummyStringParam = "dummy-param"; + //when + final Serializable convertedParam = objectUnderTest.convertParamFromServiceModel(dummyStringParam); + + then(namespaceService).shouldHaveNoInteractions(); + assertEquals(dummyStringParam, convertedParam); + } +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/ActionPermissionValidatorTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/ActionPermissionValidatorTest.java new file mode 100644 index 0000000000..2d3792c0d0 --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/ActionPermissionValidatorTest.java @@ -0,0 +1,117 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.rules; + +import static org.alfresco.service.cmr.rule.RuleType.INBOUND; +import static org.alfresco.service.cmr.rule.RuleType.OUTBOUND; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.then; + +import java.util.List; + +import junit.framework.TestCase; +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.repo.action.CompositeActionImpl; +import org.alfresco.repo.action.RuntimeActionService; +import org.alfresco.repo.action.executer.CheckOutActionExecuter; +import org.alfresco.repo.action.executer.CopyActionExecuter; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.rule.Rule; +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@Experimental +@RunWith(MockitoJUnitRunner.class) +public class ActionPermissionValidatorTest extends TestCase +{ + private static final String DUMMY_NODE_ID = "dummy-node-id"; + @Mock + private RuntimeActionService runtimeActionService; + + @InjectMocks + private ActionPermissionValidator objectUnderTest; + + @Test + public void testPositiveRulePermissionValidation() + { + //given + final CompositeActionImpl compositeAction = + new CompositeActionImpl(new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, DUMMY_NODE_ID), "composite-id"); + final Action action1 = new ActionImpl(new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, DUMMY_NODE_ID), "id-1", + CopyActionExecuter.NAME); + compositeAction.addAction(action1); + final Action action2 = new ActionImpl(new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, DUMMY_NODE_ID), "id-2", + CheckOutActionExecuter.NAME); + compositeAction.addAction(action2); + final Rule inputRule = new Rule(); + inputRule.setAction(compositeAction); + inputRule.setRuleTypes(List.of(INBOUND)); + + //when + final Rule validatedRule = objectUnderTest.validateRulePermissions(inputRule); + + then(runtimeActionService).should().verifyActionAccessRestrictions(action1); + then(runtimeActionService).should().verifyActionAccessRestrictions(action2); + then(runtimeActionService).shouldHaveNoMoreInteractions(); + + ((CompositeActionImpl) validatedRule.getAction()).getActions() + .forEach(action -> Assertions.assertThat(action.getParameterValue("actionContext")).isEqualTo("rule")); + } + + @Test + public void testNegativeRulePermissionValidation() + { + //given + final CompositeActionImpl compositeAction = + new CompositeActionImpl(new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, DUMMY_NODE_ID), "composite-id"); + final Action action1 = new ActionImpl(new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, DUMMY_NODE_ID), "id-1", + CopyActionExecuter.NAME); + compositeAction.addAction(action1); + final Action action2 = new ActionImpl(new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, DUMMY_NODE_ID), "id-2", + CheckOutActionExecuter.NAME); + compositeAction.addAction(action2); + final Rule inputRule = new Rule(); + inputRule.setAction(compositeAction); + inputRule.setRuleTypes(List.of(OUTBOUND)); + + //when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy(() -> objectUnderTest.validateRulePermissions(inputRule)); + + then(runtimeActionService).should().verifyActionAccessRestrictions(action1); + then(runtimeActionService).should().verifyActionAccessRestrictions(action2); + then(runtimeActionService).shouldHaveNoMoreInteractions(); + } + +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/NodeValidatorTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/NodeValidatorTest.java new file mode 100644 index 0000000000..f71f32156e --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/NodeValidatorTest.java @@ -0,0 +1,441 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.rules; + +import static org.alfresco.model.ContentModel.TYPE_FOLDER; +import static org.alfresco.model.ContentModel.TYPE_SYSTEM_FOLDER; +import static org.alfresco.repo.rule.RuleModel.TYPE_RULE; +import static org.alfresco.rest.api.model.rules.RuleSet.DEFAULT_ID; +import static org.alfresco.service.cmr.security.AccessStatus.ALLOWED; +import static org.alfresco.service.cmr.security.AccessStatus.DENIED; +import static org.alfresco.service.cmr.security.PermissionService.CHANGE_PERMISSIONS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.reset; + +import java.util.Set; + +import org.alfresco.model.ContentModel; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.Node; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.cmr.security.PermissionService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class NodeValidatorTest +{ + + private static final String FOLDER_NODE_ID = "dummy-folder-node-id"; + private static final String LINK_TO_NODE_ID = "dummy-link-to-node-id"; + private static final String RULE_SET_ID = "dummy-rule-set-id"; + private static final String RULE_ID = "dummy-rule-id"; + private static final String PARENT_NODE_ID = "dummy-parent-node-id"; + private static final NodeRef folderNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, FOLDER_NODE_ID); + private static final NodeRef ruleSetNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, RULE_SET_ID); + private static final NodeRef ruleNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, RULE_ID); + private static final NodeRef parentNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_NODE_ID); + + @Mock + private Nodes nodesMock; + + @Mock + private Node ruleSetNodeMock; + + @Mock + private PermissionService permissionServiceMock; + + @Mock + private RuleService ruleServiceMock; + + @Mock + private NodeService nodeServiceMock; + + @Mock + private ChildAssociationRef primaryParentMock; + + @InjectMocks + private NodeValidator nodeValidator; + + @Before + public void setUp() throws Exception + { + MockitoAnnotations.openMocks(this); + given(nodesMock.validateOrLookupNode(eq(FOLDER_NODE_ID), any())).willReturn(folderNodeRef); + given(nodesMock.validateNode(RULE_SET_ID)).willReturn(ruleSetNodeRef); + given(nodesMock.validateNode(RULE_ID)).willReturn(ruleNodeRef); + given(nodesMock.nodeMatches(any(), any(), any())).willReturn(true); + given(permissionServiceMock.hasReadPermission(any())).willReturn(ALLOWED); + given(permissionServiceMock.hasPermission(any(), any())).willReturn(ALLOWED); + } + + @Test + public void testValidateFolderNode() + { + // when + final NodeRef nodeRef = nodeValidator.validateFolderNode(FOLDER_NODE_ID, false); + + then(nodesMock).should().validateOrLookupNode(FOLDER_NODE_ID, null); + then(nodesMock).should().nodeMatches(folderNodeRef, Set.of(TYPE_FOLDER), null); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(permissionServiceMock).should().hasReadPermission(folderNodeRef); + then(permissionServiceMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + + assertThat(nodeRef).isNotNull().isEqualTo(folderNodeRef); + } + + @Test + public void testValidateFolderNode_notExistingFolder() + { + given(nodesMock.validateOrLookupNode(any(), any())).willThrow(new EntityNotFoundException(FOLDER_NODE_ID)); + + //when + assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy( + () -> nodeValidator.validateFolderNode(FOLDER_NODE_ID, false)); + + then(nodesMock).should().validateOrLookupNode(FOLDER_NODE_ID, null); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + + @Test + public void testValidateFolderNode_notMatchingTypeFolder() + { + given(nodesMock.nodeMatches(any(), eq(Set.of(TYPE_FOLDER)), any())).willReturn(false); + + // when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy( + () -> nodeValidator.validateFolderNode(FOLDER_NODE_ID, false)); + + then(nodesMock).should().validateOrLookupNode(FOLDER_NODE_ID, null); + then(nodesMock).should().nodeMatches(folderNodeRef, Set.of(TYPE_FOLDER), null); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + + @Test + public void testValidateFolderNode_noReadPermission() + { + given(permissionServiceMock.hasReadPermission(any())).willReturn(DENIED); + + // when + assertThatExceptionOfType(PermissionDeniedException.class).isThrownBy( + () -> nodeValidator.validateFolderNode(FOLDER_NODE_ID, false)); + + then(permissionServiceMock).should().hasReadPermission(folderNodeRef); + then(permissionServiceMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + + @Test + public void testValidateFolderNode_noChangePermission() + { + given(permissionServiceMock.hasPermission(any(), any())).willReturn(DENIED); + + // when + assertThatExceptionOfType(PermissionDeniedException.class).isThrownBy(() -> + nodeValidator.validateFolderNode(folderNodeRef.getId(), true)); + + then(permissionServiceMock).should().hasPermission(folderNodeRef, CHANGE_PERMISSIONS); + then(permissionServiceMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void validateRuleSetNode() + { + given(ruleServiceMock.isRuleSetAssociatedWithFolder(any(), any())).willReturn(true); + + // when + final NodeRef nodeRef = nodeValidator.validateRuleSetNode(RULE_SET_ID, folderNodeRef); + + then(nodesMock).should().validateNode(RULE_SET_ID); + then(nodesMock).should().nodeMatches(ruleSetNodeRef, Set.of(TYPE_SYSTEM_FOLDER), null); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).should().isRuleSetAssociatedWithFolder(ruleSetNodeRef, folderNodeRef); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + then(permissionServiceMock).shouldHaveNoInteractions(); + + assertThat(nodeRef).isNotNull().isEqualTo(ruleSetNodeRef); + } + + @Test + public void testValidateRuleSetNodeNoParentId() + { + given(nodesMock.getNode(any())).willReturn(ruleSetNodeMock); + given(nodeServiceMock.getPrimaryParent(any())).willReturn(primaryParentMock); + given(primaryParentMock.getParentRef()).willReturn(parentNodeRef); + + //when + final NodeRef nodeRef = nodeValidator.validateRuleSetNode(LINK_TO_NODE_ID,true); + + assertThat(nodeRef).isNotNull().isEqualTo(parentNodeRef); + + } + + @Test + public void validateRuleSetNode_defaultId() + { + given(ruleServiceMock.getRuleSetNode(any())).willReturn(ruleSetNodeRef); + + // when + final NodeRef nodeRef = nodeValidator.validateRuleSetNode(DEFAULT_ID, folderNodeRef); + + then(ruleServiceMock).should().getRuleSetNode(folderNodeRef); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + then(nodesMock).shouldHaveNoInteractions(); + then(permissionServiceMock).shouldHaveNoInteractions(); + + assertThat(nodeRef).isNotNull().isEqualTo(ruleSetNodeRef); + } + + @Test + public void testValidateRuleSetNode_notExistingRuleSet() + { + given(nodesMock.validateNode(RULE_SET_ID)).willThrow(new EntityNotFoundException(RULE_SET_ID)); + + //when + assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy( + () -> nodeValidator.validateRuleSetNode(RULE_SET_ID, folderNodeRef)); + + then(nodesMock).should().validateNode(RULE_SET_ID); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + + @Test + public void testValidateRuleSetNode_notMatchingTypeSystemFolder() + { + given(nodesMock.nodeMatches(any(), eq(Set.of(TYPE_SYSTEM_FOLDER)), any())).willReturn(false); + + // when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy( + () -> nodeValidator.validateRuleSetNode(RULE_SET_ID, folderNodeRef)); + + then(nodesMock).should().validateNode(RULE_SET_ID); + then(nodesMock).should().nodeMatches(ruleSetNodeRef, Set.of(TYPE_SYSTEM_FOLDER), null); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + + @Test + public void testValidateRuleSetNode_notExistingDefaultRuleSet() + { + given(ruleServiceMock.getRuleSetNode(folderNodeRef)).willReturn(null); + + // when + assertThatExceptionOfType(RelationshipResourceNotFoundException.class).isThrownBy( + () -> nodeValidator.validateRuleSetNode(DEFAULT_ID, folderNodeRef)); + + then(ruleServiceMock).should().getRuleSetNode(folderNodeRef); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + then(nodesMock).shouldHaveNoInteractions(); + } + + @Test + public void testValidateRuleSetNode_notAssociatedRuleSetToFolder() + { + given(ruleServiceMock.isRuleSetAssociatedWithFolder(any(), any())).willReturn(false); + + // when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy( + () -> nodeValidator.validateRuleSetNode(RULE_SET_ID, folderNodeRef)); + + then(ruleServiceMock).should().isRuleSetAssociatedWithFolder(ruleSetNodeRef, folderNodeRef); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void validateRuleNode() + { + given(ruleServiceMock.isRuleAssociatedWithRuleSet(any(), any())).willReturn(true); + + // when + final NodeRef nodeRef = nodeValidator.validateRuleNode(RULE_ID, ruleSetNodeRef); + + then(nodesMock).should().validateNode(RULE_ID); + then(nodesMock).should().nodeMatches(ruleNodeRef, Set.of(TYPE_RULE), null); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).should().isRuleAssociatedWithRuleSet(ruleNodeRef, ruleSetNodeRef); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + then(permissionServiceMock).shouldHaveNoInteractions(); + + assertThat(nodeRef).isNotNull().isEqualTo(ruleNodeRef); + } + + @Test + public void validateRuleNode_nullRuleSet() + { + // when + final NodeRef nodeRef = nodeValidator.validateRuleNode(RULE_ID, null); + + then(nodesMock).should().validateNode(RULE_ID); + then(nodesMock).should().nodeMatches(ruleNodeRef, Set.of(TYPE_RULE), null); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + then(permissionServiceMock).shouldHaveNoInteractions(); + + assertThat(nodeRef).isNotNull().isEqualTo(ruleNodeRef); + } + + @Test + public void testValidateRuleNode_notExistingRule() + { + given(nodesMock.validateNode(RULE_ID)).willThrow(new EntityNotFoundException(RULE_ID)); + + //when + assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy( + () -> nodeValidator.validateRuleNode(RULE_ID, ruleSetNodeRef)); + + then(nodesMock).should().validateNode(RULE_ID); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + + @Test + public void testValidateRuleNode_notMatchingTypeRule() + { + given(nodesMock.nodeMatches(any(), eq(Set.of(TYPE_RULE)), any())).willReturn(false); + + // when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy( + () -> nodeValidator.validateRuleNode(RULE_ID, ruleSetNodeRef)); + + then(nodesMock).should().validateNode(RULE_ID); + then(nodesMock).should().nodeMatches(ruleNodeRef, Set.of(TYPE_RULE), null); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + + @Test + public void testValidateRuleNode_notAssociatedRuleToRuleSet() + { + given(ruleServiceMock.isRuleAssociatedWithRuleSet(any(), any())).willReturn(false); + + // when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy( + () -> nodeValidator.validateRuleNode(RULE_ID, ruleSetNodeRef)); + + then(ruleServiceMock).should().isRuleAssociatedWithRuleSet(ruleNodeRef, ruleSetNodeRef); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testIsRuleSetNode() + { + //resetting mock to bypass setup method + resetNodesMock(); + + boolean actual = nodeValidator.isRuleSetNode(RULE_SET_ID); + Assert.assertTrue(actual); + } + + + @Test + public void testIsNotRuleSetNode() + { + //resetting mock to bypass setup method + resetNodesMock(); + + //using an id that doesn't belong to a ruleset node + boolean actual = nodeValidator.isRuleSetNode(FOLDER_NODE_ID); + Assert.assertFalse(actual); + } + + @Test + public void testIsRuleSetNotNullAndShared() + { + given(ruleServiceMock.isRuleSetShared(any())).willReturn(true); + + // when + final boolean shared = nodeValidator.isRuleSetNotNullAndShared(ruleSetNodeRef); + + then(ruleServiceMock).should().isRuleSetShared(ruleSetNodeRef); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + + assertThat(shared).isTrue(); + } + + @Test + public void testIsRuleSetNotNullAndShared_nullRuleSetNode() + { + // when + final boolean shared = nodeValidator.isRuleSetNotNullAndShared(null); + + then(ruleServiceMock).shouldHaveNoInteractions(); + + assertThat(shared).isFalse(); + } + + @Test + public void testIsRuleSetNotNullAndShared_withoutRuleSetAndWithFolder() + { + given(ruleServiceMock.getRuleSetNode(any())).willReturn(ruleSetNodeRef); + + // when + nodeValidator.isRuleSetNotNullAndShared(null, folderNodeRef); + + then(ruleServiceMock).should().getRuleSetNode(folderNodeRef); + then(ruleServiceMock).should().isRuleSetShared(ruleSetNodeRef); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testIsRuleSetNotNullAndShared_withRuleSetAndWithFolder() + { + // when + nodeValidator.isRuleSetNotNullAndShared(ruleSetNodeRef, folderNodeRef); + + then(ruleServiceMock).should().isRuleSetShared(ruleSetNodeRef); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + } + + private void resetNodesMock() { + reset(nodesMock); + given(nodesMock.validateOrLookupNode(eq(FOLDER_NODE_ID), any())).willReturn(folderNodeRef); + given(nodesMock.validateNode(RULE_SET_ID)).willReturn(ruleSetNodeRef); + given(nodesMock.validateNode(RULE_ID)).willReturn(ruleNodeRef); + given(nodesMock.nodeMatches(ruleSetNodeRef, Set.of(ContentModel.TYPE_SYSTEM_FOLDER), null)).willReturn(true); + } +} \ No newline at end of file diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleLoaderTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleLoaderTest.java new file mode 100644 index 0000000000..9af293534f --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleLoaderTest.java @@ -0,0 +1,149 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.impl.rules; + +import static java.util.Collections.emptyList; + +import static org.alfresco.rest.api.impl.rules.RuleLoader.IS_SHARED; +import static org.alfresco.rest.api.model.rules.RuleTrigger.OUTBOUND; +import static org.alfresco.rest.api.model.rules.RuleTrigger.UPDATE; +import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import java.util.List; + +import org.alfresco.rest.api.model.mapper.RestModelMapper; +import org.alfresco.rest.api.model.rules.Rule; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.rule.RuleService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** Unit tests for {@link RuleLoader}. */ +@RunWith(MockitoJUnitRunner.class) +public class RuleLoaderTest +{ + private static final String NODE_ID = "node-id"; + private static final NodeRef NODE_REF = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, NODE_ID); + private static final String TITLE = "title"; + private static final String DESCRIPTION = "description"; + private static final boolean ENABLED = true; + private static final boolean INHERITABLE = true; + private static final boolean EXECUTE_ASYNCHRONOUSLY = false; + private static final List TRIGGERS = List.of("update", "outbound"); + private static final NodeRef RULE_SET_NODE = new NodeRef("rule://set/"); + + @Mock + private RuleService ruleServiceMock; + @Mock + private NodeValidator nodeValidatorMock; + @Mock + private RestModelMapper ruleMapperMock; + + private org.alfresco.service.cmr.rule.Rule serviceRule = createServiceRule(); + + @InjectMocks + private RuleLoader ruleLoader; + + @Test + public void testLoadRule() + { + final Rule restModelRule = getRestModelRule(); + given(ruleMapperMock.toRestModel(serviceRule)).willReturn(restModelRule); + + //when + Rule rule = ruleLoader.loadRule(serviceRule, emptyList()); + + Rule expected = Rule.builder().id(NODE_ID) + .name(TITLE) + .description(DESCRIPTION) + .isEnabled(ENABLED) + .isInheritable(INHERITABLE) + .isAsynchronous(EXECUTE_ASYNCHRONOUSLY) + .triggers(List.of(UPDATE, OUTBOUND)).create(); + assertThat(rule).isEqualTo(expected); + } + + @Test + public void testLoadRule_noExceptionWithNullInclude() + { + //when + ruleLoader.loadRule(serviceRule, null); + + then(ruleMapperMock).should().toRestModel(serviceRule); + then(ruleMapperMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testLoadRule_includeIsShared() + { + // Simulate the rule set being shared. + final Rule restModelRule = getRestModelRule(); + given(ruleServiceMock.getRuleSetNode(NODE_REF)).willReturn(RULE_SET_NODE); + given(nodeValidatorMock.isRuleSetNotNullAndShared(RULE_SET_NODE)).willReturn(true); + given(ruleMapperMock.toRestModel(serviceRule)).willReturn(restModelRule); + + Rule rule = ruleLoader.loadRule(serviceRule, List.of(IS_SHARED)); + + then(ruleMapperMock).should().toRestModel(serviceRule); + then(ruleMapperMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).should().getRuleSetNode(NODE_REF); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + then(nodeValidatorMock).should().isRuleSetNotNullAndShared(RULE_SET_NODE); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + assertThat(rule).extracting("isShared").isEqualTo(true); + } + + private org.alfresco.service.cmr.rule.Rule createServiceRule() + { + org.alfresco.service.cmr.rule.Rule rule = new org.alfresco.service.cmr.rule.Rule(); + rule.setNodeRef(NODE_REF); + rule.setTitle(TITLE); + rule.setDescription(DESCRIPTION); + rule.setRuleDisabled(!ENABLED); + rule.applyToChildren(INHERITABLE); + rule.setExecuteAsynchronously(EXECUTE_ASYNCHRONOUSLY); + rule.setRuleTypes(TRIGGERS); + return rule; + } + + private Rule getRestModelRule() + { + return Rule.builder().id(NODE_ID) + .name(TITLE) + .description(DESCRIPTION) + .isEnabled(ENABLED) + .isInheritable(INHERITABLE) + .isAsynchronous(EXECUTE_ASYNCHRONOUSLY) + .triggers(List.of(UPDATE, OUTBOUND)).create(); + } + +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSetLoaderTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSetLoaderTest.java new file mode 100644 index 0000000000..c62e6806de --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSetLoaderTest.java @@ -0,0 +1,185 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.impl.rules; + +import static org.alfresco.rest.api.impl.rules.RuleSetLoader.INCLUSION_TYPE; +import static org.alfresco.rest.api.impl.rules.RuleSetLoader.INHERITED_BY; +import static org.alfresco.rest.api.impl.rules.RuleSetLoader.IS_INHERITED; +import static org.alfresco.rest.api.impl.rules.RuleSetLoader.IS_LINKED_TO; +import static org.alfresco.rest.api.impl.rules.RuleSetLoader.LINKED_TO_BY; +import static org.alfresco.rest.api.impl.rules.RuleSetLoader.OWNING_FOLDER; +import static org.alfresco.rest.api.model.rules.InclusionType.INHERITED; +import static org.alfresco.rest.api.model.rules.InclusionType.LINKED; +import static org.alfresco.rest.api.model.rules.InclusionType.OWNED; +import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; + +import java.util.List; + +import junit.framework.TestCase; +import org.alfresco.rest.api.model.rules.RuleSet; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.rule.RuleService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** Unit tests for {@link RuleSetLoader}. */ +@Experimental +@RunWith (MockitoJUnitRunner.class) +public class RuleSetLoaderTest extends TestCase +{ + private static final String FOLDER_ID = "dummy-folder-id"; + private static final String RULE_SET_ID = "dummy-rule-set-id"; + private static final NodeRef FOLDER_NODE = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, FOLDER_ID); + private static final NodeRef RULE_SET_NODE = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, RULE_SET_ID); + private static final String LINKING_FOLDER_ID = "linking-folder"; + private static final NodeRef LINKING_FOLDER = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, LINKING_FOLDER_ID); + private static final NodeRef INHERITING_FOLDER = new NodeRef("inheriting://folder/"); + + @InjectMocks + private RuleSetLoader ruleSetLoader; + @Mock + private NodeService nodeServiceMock; + @Mock + private RuleService ruleServiceMock; + @Mock + private ChildAssociationRef ruleSetAssociationMock; + @Mock + private ChildAssociationRef linkAssociationMock; + + @Before + @Override + public void setUp() + { + given(ruleSetAssociationMock.getParentRef()).willReturn(FOLDER_NODE); + given(nodeServiceMock.getPrimaryParent(RULE_SET_NODE)).willReturn(ruleSetAssociationMock); + + given(linkAssociationMock.getParentRef()).willReturn(LINKING_FOLDER); + given(nodeServiceMock.getParentAssocs(RULE_SET_NODE)).willReturn(List.of(ruleSetAssociationMock, linkAssociationMock)); + + given(ruleServiceMock.getFoldersInheritingRuleSet(eq(RULE_SET_NODE), anyInt())).willReturn(List.of(INHERITING_FOLDER)); + given(ruleServiceMock.getFoldersLinkingToRuleSet(eq(RULE_SET_NODE), anyInt())).willReturn(List.of(LINKING_FOLDER)); + } + + @Test + public void testLoadRuleSet_noIncludes() + { + // Call the method under test. + RuleSet actual = ruleSetLoader.loadRuleSet(RULE_SET_NODE, FOLDER_NODE, null); + + RuleSet expected = RuleSet.builder().id(RULE_SET_ID).create(); + assertEquals(expected, actual); + } + + @Test + public void testLoadRuleSet_includeOwningFolder() + { + // Call the method under test. + RuleSet actual = ruleSetLoader.loadRuleSet(RULE_SET_NODE, FOLDER_NODE, List.of(OWNING_FOLDER)); + + RuleSet expected = RuleSet.builder().id(RULE_SET_ID).owningFolder(FOLDER_NODE).create(); + assertEquals(expected, actual); + } + + @Test + public void testLoadRuleSet_includeInclusionType() + { + // Call the method under test. + RuleSet actual = ruleSetLoader.loadRuleSet(RULE_SET_NODE, FOLDER_NODE, List.of(INCLUSION_TYPE)); + + RuleSet expected = RuleSet.builder().id(RULE_SET_ID).inclusionType(OWNED).create(); + assertEquals(expected, actual); + } + + @Test + public void testLoadRuleSet_linkedInclusionType() + { + // Call the method under test. + RuleSet actual = ruleSetLoader.loadRuleSet(RULE_SET_NODE, LINKING_FOLDER, List.of(INCLUSION_TYPE)); + + RuleSet expected = RuleSet.builder().id(RULE_SET_ID).inclusionType(LINKED).create(); + assertEquals(expected, actual); + } + + @Test + public void testLoadRuleSet_inheritedInclusionType() + { + // Call the method under test. + RuleSet actual = ruleSetLoader.loadRuleSet(RULE_SET_NODE, INHERITING_FOLDER, List.of(INCLUSION_TYPE)); + + RuleSet expected = RuleSet.builder().id(RULE_SET_ID).inclusionType(INHERITED).create(); + assertEquals(expected, actual); + } + + @Test + public void testLoadRuleSet_inheritedBy() + { + // Call the method under test. + RuleSet actual = ruleSetLoader.loadRuleSet(RULE_SET_NODE, INHERITING_FOLDER, List.of(INHERITED_BY)); + + RuleSet expected = RuleSet.builder().id(RULE_SET_ID).inheritedBy(List.of(INHERITING_FOLDER)).create(); + assertEquals(expected, actual); + } + + @Test + public void testLoadRuleSet_linkedToBy() + { + // Call the method under test. + RuleSet actual = ruleSetLoader.loadRuleSet(RULE_SET_NODE, FOLDER_NODE, List.of(LINKED_TO_BY)); + + RuleSet expected = RuleSet.builder().id(RULE_SET_ID).linkedToBy(List.of(LINKING_FOLDER)).create(); + assertEquals(expected, actual); + } + + @Test + public void testLoadRuleSet_isInherited() + { + // Call the method under test. + RuleSet actual = ruleSetLoader.loadRuleSet(RULE_SET_NODE, FOLDER_NODE, List.of(IS_INHERITED)); + + RuleSet expected = RuleSet.builder().id(RULE_SET_ID).isInherited(true).create(); + assertEquals(expected, actual); + } + + @Test + public void testLoadRuleSet_isLinkedTo() + { + // Call the method under test. + RuleSet actual = ruleSetLoader.loadRuleSet(RULE_SET_NODE, FOLDER_NODE, List.of(IS_LINKED_TO)); + + RuleSet expected = RuleSet.builder().id(RULE_SET_ID).isLinkedTo(true).create(); + assertEquals(expected, actual); + } +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSetsImplTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSetsImplTest.java new file mode 100644 index 0000000000..f0402b304c --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSetsImplTest.java @@ -0,0 +1,483 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.impl.rules; + +import static java.util.Collections.emptyList; + +import static org.alfresco.rest.api.impl.rules.RuleSetLoader.RULE_IDS; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.util.Collection; +import java.util.List; + +import junit.framework.TestCase; +import org.alfresco.repo.rule.RuleModel; +import org.alfresco.repo.rule.RuntimeRuleService; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.rest.api.model.rules.RuleSet; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.rule.RuleService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; + +/** Unit tests for {@link RuleSetsImpl}. */ +@Experimental +@RunWith (MockitoJUnitRunner.class) +public class RuleSetsImplTest extends TestCase +{ + private static final String FOLDER_ID = "dummy-folder-id"; + private static final String LINK_TO_NODE_ID = "dummy-link-to-node-id"; + private static final String RULE_SET_ID = "dummy-rule-set-id"; + private static final NodeRef FOLDER_NODE = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, FOLDER_ID); + private static final NodeRef LINK_TO_NODE = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, LINK_TO_NODE_ID); + private static final NodeRef RULE_SET_NODE = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, RULE_SET_ID); + private static final Paging PAGING = Paging.DEFAULT; + private static final List INCLUDES = List.of("dummy-includes"); + + @InjectMocks + private RuleSetsImpl ruleSets; + @Mock + private RuleSetLoader ruleSetLoaderMock; + @Mock + private NodeValidator nodeValidatorMock; + @Mock + private NodeService nodeServiceMock; + @Mock + private RuleService ruleServiceMock; + @Mock + private RuntimeRuleService runtimeRuleServiceMock; + @Mock + private RuleSet ruleSetMock; + @Mock + private ChildAssociationRef assocRef; + + @Before + @Override + public void setUp() + { + MockitoAnnotations.openMocks(this); + + given(nodeValidatorMock.validateFolderNode(eq(LINK_TO_NODE_ID), anyBoolean())).willReturn(LINK_TO_NODE); + given(nodeValidatorMock.validateRuleSetNode(LINK_TO_NODE_ID,false)).willReturn(LINK_TO_NODE); + given(nodeValidatorMock.validateFolderNode(eq(FOLDER_ID), anyBoolean())).willReturn(FOLDER_NODE); + given(nodeValidatorMock.validateRuleSetNode(RULE_SET_ID, FOLDER_NODE)).willReturn(RULE_SET_NODE); + + given(ruleServiceMock.getRuleSetNode(FOLDER_NODE)).willReturn(RULE_SET_NODE); + given(ruleServiceMock.getNodesSupplyingRuleSets(FOLDER_NODE)).willReturn(List.of(FOLDER_NODE)); + + given(ruleSetLoaderMock.loadRuleSet(RULE_SET_NODE, FOLDER_NODE, INCLUDES)).willReturn(ruleSetMock); + } + + @Test + public void testGetRuleSets() + { + // Call the method under test. + CollectionWithPagingInfo actual = ruleSets.getRuleSets(FOLDER_ID, INCLUDES, PAGING); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_ID, false); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + + then(ruleServiceMock).should().getNodesSupplyingRuleSets(FOLDER_NODE); + then(ruleServiceMock).should().getRuleSetNode(FOLDER_NODE); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + + Collection expected = List.of(ruleSetMock); + assertEquals(expected, actual.getCollection()); + assertEquals(PAGING, actual.getPaging()); + } + + @Test + public void testGetZeroRuleSets() + { + // Simulate no rule sets for the folder. + given(ruleServiceMock.getRuleSetNode(FOLDER_NODE)).willReturn(null); + + // Call the method under test. + CollectionWithPagingInfo actual = ruleSets.getRuleSets(FOLDER_ID, INCLUDES, PAGING); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_ID, false); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + + then(ruleServiceMock).should().getNodesSupplyingRuleSets(FOLDER_NODE); + then(ruleServiceMock).should().getRuleSetNode(FOLDER_NODE); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + + assertEquals(emptyList(), actual.getCollection()); + assertEquals(PAGING, actual.getPaging()); + } + + @Test + public void testOnlyGetPermittedRuleSets() + { + // Simulate a private folder with a rule set that the current user can't access. + NodeRef privateFolder = new NodeRef("private://folder/"); + NodeRef privateRuleSetNode = new NodeRef("private://rule/set/node/"); + given(ruleServiceMock.getRuleSetNode(privateFolder)).willReturn(privateRuleSetNode); + given(ruleServiceMock.getNodesSupplyingRuleSets(FOLDER_NODE)).willReturn(List.of(FOLDER_NODE, privateFolder)); + given(ruleSetLoaderMock.loadRuleSet(eq(privateRuleSetNode), any(NodeRef.class), any(List.class))) + .willThrow(new AccessDeniedException("Cannot access private rule set.")); + + // Call the method under test. + CollectionWithPagingInfo actual = ruleSets.getRuleSets(FOLDER_ID, INCLUDES, PAGING); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_ID, false); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + + then(ruleServiceMock).should().getNodesSupplyingRuleSets(FOLDER_NODE); + then(ruleServiceMock).should().getRuleSetNode(FOLDER_NODE); + then(ruleServiceMock).should().getRuleSetNode(privateFolder); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + + // Check we only get the accessible rule set back. + Collection expected = List.of(ruleSetMock); + assertEquals(expected, actual.getCollection()); + assertEquals(PAGING, actual.getPaging()); + } + + /** Check that a folder with a parent and grandparent can inherit rule sets from the grandparent, even if the parent has no rules. */ + @Test + public void testGetInheritedRuleSets() + { + // Simulate a parent node without a rule set. + NodeRef parentNode = new NodeRef("parent://node/"); + // Simulate a grandparent node providing a rule set. + NodeRef grandparentNode = new NodeRef("grandparent://node/"); + RuleSet grandparentRuleSet = mock(RuleSet.class); + NodeRef grandparentRuleSetNode = new NodeRef("grandparent://rule-set/"); + given(ruleServiceMock.getRuleSetNode(grandparentNode)).willReturn(grandparentRuleSetNode); + given(ruleSetLoaderMock.loadRuleSet(grandparentRuleSetNode, FOLDER_NODE, INCLUDES)).willReturn(grandparentRuleSet); + // These should be returned with the highest in hierarchy first. + given(ruleServiceMock.getNodesSupplyingRuleSets(FOLDER_NODE)).willReturn(List.of(grandparentNode, parentNode, FOLDER_NODE)); + + // Call the method under test. + CollectionWithPagingInfo actual = ruleSets.getRuleSets(FOLDER_ID, INCLUDES, PAGING); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_ID, false); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + + then(ruleServiceMock).should().getNodesSupplyingRuleSets(FOLDER_NODE); + then(ruleServiceMock).should().getRuleSetNode(grandparentNode); + then(ruleServiceMock).should().getRuleSetNode(parentNode); + then(ruleServiceMock).should().getRuleSetNode(FOLDER_NODE); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + + Collection expected = List.of(grandparentRuleSet, ruleSetMock); + assertEquals(expected, actual.getCollection()); + assertEquals(PAGING, actual.getPaging()); + } + + /** When getting rule sets then only the first instance of each rule set should be included (ancestor first). */ + @Test + public void testGetDuplicateRuleSets() + { + // Simulate a grandparent, parent and child with the grandparent linking to the child's rule set. + NodeRef grandparentNode = new NodeRef("grandparent://node/"); + given(ruleServiceMock.getRuleSetNode(grandparentNode)).willReturn(RULE_SET_NODE); + NodeRef parentNode = new NodeRef("parent://node/"); + RuleSet parentRuleSet = mock(RuleSet.class); + NodeRef parentRuleSetNode = new NodeRef("parent://rule-set/"); + given(ruleServiceMock.getRuleSetNode(parentNode)).willReturn(parentRuleSetNode); + given(ruleSetLoaderMock.loadRuleSet(parentRuleSetNode, FOLDER_NODE, INCLUDES)).willReturn(parentRuleSet); + // These should be returned with the highest in hierarchy first. + given(ruleServiceMock.getNodesSupplyingRuleSets(FOLDER_NODE)).willReturn(List.of(grandparentNode, parentNode, FOLDER_NODE)); + + // Call the method under test. + CollectionWithPagingInfo actual = ruleSets.getRuleSets(FOLDER_ID, INCLUDES, PAGING); + + // The grandparent's linked rule set should be first and only appear once. + Collection expected = List.of(ruleSetMock, parentRuleSet); + assertEquals(expected, actual.getCollection()); + assertEquals(PAGING, actual.getPaging()); + } + + @Test + public void testGetRuleSetById() + { + // Call the method under test. + RuleSet actual = ruleSets.getRuleSetById(FOLDER_ID, RULE_SET_ID, INCLUDES); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_ID, false); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + + assertEquals(ruleSetMock, actual); + } + + @Test + public void testLinkToFolderRuleSet() + { + NodeRef childNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "dummy-child-id"); + + given(ruleServiceMock.hasRules(any(NodeRef.class))).willReturn(true, false); + given(runtimeRuleServiceMock.getSavedRuleFolderAssoc(any(NodeRef.class))).willReturn(assocRef); + given(assocRef.getChildRef()).willReturn(childNodeRef); + + //when + String actual = ruleSets.linkToRuleSet(FOLDER_ID,LINK_TO_NODE_ID).getId(); + + then(ruleServiceMock).should().hasRules(LINK_TO_NODE); + then(ruleServiceMock).should().hasRules(FOLDER_NODE); + then(runtimeRuleServiceMock).should().getSavedRuleFolderAssoc(LINK_TO_NODE); + then(runtimeRuleServiceMock).shouldHaveNoMoreInteractions(); + then(nodeServiceMock).should().addChild(FOLDER_NODE, childNodeRef, RuleModel.ASSOC_RULE_FOLDER, RuleModel.ASSOC_RULE_FOLDER); + then(nodeServiceMock).shouldHaveNoMoreInteractions(); + + assertEquals(childNodeRef.getId(),actual); + } + + @Test + public void testLinkToRuleSet() + { + NodeRef childNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "dummy-child-id"); + + given(nodeValidatorMock.isRuleSetNode(any())).willReturn(true); + given(ruleServiceMock.hasRules(any(NodeRef.class))).willReturn(true, false); + given(runtimeRuleServiceMock.getSavedRuleFolderAssoc(any(NodeRef.class))).willReturn(assocRef); + given(assocRef.getChildRef()).willReturn(childNodeRef); + + //when + String actual = ruleSets.linkToRuleSet(FOLDER_ID,LINK_TO_NODE_ID).getId(); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_ID,true); + then(nodeValidatorMock).should().isRuleSetNode(LINK_TO_NODE_ID); + then(nodeValidatorMock).should().validateRuleSetNode(LINK_TO_NODE_ID,false); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).should().hasRules(LINK_TO_NODE); + then(ruleServiceMock).should().hasRules(FOLDER_NODE); + then(runtimeRuleServiceMock).should().getSavedRuleFolderAssoc(LINK_TO_NODE); + then(runtimeRuleServiceMock).shouldHaveNoMoreInteractions(); + then(nodeServiceMock).should().addChild(FOLDER_NODE, childNodeRef, RuleModel.ASSOC_RULE_FOLDER, RuleModel.ASSOC_RULE_FOLDER); + then(nodeServiceMock).shouldHaveNoMoreInteractions(); + + assertEquals(childNodeRef.getId(),actual); + } + + @Test + public void testLinkToRuleSet_targetFolderHasNoRules() + { + given(ruleServiceMock.hasRules(LINK_TO_NODE)).willReturn(false); + + //when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy( + () -> ruleSets.linkToRuleSet(FOLDER_ID, LINK_TO_NODE_ID) + ); + + then(nodeServiceMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).should().hasRules(LINK_TO_NODE); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + then(runtimeRuleServiceMock).shouldHaveNoInteractions(); + } + + @Test + public void testLinkToRuleSet_folderShouldntHavePreExistingRules() + { + given(ruleServiceMock.hasRules(any(NodeRef.class))).willReturn(true, true); + + //when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy( + () -> ruleSets.linkToRuleSet(FOLDER_ID, LINK_TO_NODE_ID)); + + then(nodeServiceMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).should().hasRules(LINK_TO_NODE); + then(ruleServiceMock).should().hasRules(FOLDER_NODE); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + then(runtimeRuleServiceMock).shouldHaveNoInteractions(); + } + + @Test + public void testUnlinkRuleSet() + { + given(ruleServiceMock.isLinkedToRuleNode(FOLDER_NODE)).willReturn(true); + + //when + ruleSets.unlinkRuleSet(FOLDER_ID,RULE_SET_ID); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_ID,true); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID,FOLDER_NODE); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).should().isLinkedToRuleNode(FOLDER_NODE); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + then(nodeServiceMock).should().removeAspect(FOLDER_NODE,RuleModel.ASPECT_RULES); + then(nodeServiceMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testUnlinkRuleSet_folderIsNotLinkedToRuleSet() + { + given(ruleServiceMock.isLinkedToRuleNode(FOLDER_NODE)).willReturn(false); + + //when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy( + () -> ruleSets.unlinkRuleSet(FOLDER_ID,RULE_SET_ID)); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_ID,true); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID,FOLDER_NODE); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).should().isLinkedToRuleNode(FOLDER_NODE); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + then(nodeServiceMock).shouldHaveNoInteractions(); + } + + @Test + public void testUpdateRuleSet() + { + given(ruleSetMock.getId()).willReturn(RULE_SET_ID); + given(nodeValidatorMock.validateFolderNode(FOLDER_ID, false)).willReturn(FOLDER_NODE); + given(nodeValidatorMock.validateRuleSetNode(RULE_SET_ID, FOLDER_NODE)).willReturn(RULE_SET_NODE); + RuleSet ruleSetResponse = mock(RuleSet.class); + given(ruleSetLoaderMock.loadRuleSet(RULE_SET_NODE, FOLDER_NODE, emptyList())).willReturn(ruleSetResponse); + + //when + RuleSet ruleSet = ruleSets.updateRuleSet(FOLDER_ID, ruleSetMock, emptyList()); + + assertEquals("Unexpected rule set returned.", ruleSetResponse, ruleSet); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_ID, false); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE); + then(ruleSetLoaderMock).should().loadRuleSet(RULE_SET_NODE, FOLDER_NODE, emptyList()); + } + + /** Simulate rules being reordered from [RuleA, RuleB] to [RuleB, RuleA]. */ + @Test + public void testUpdateRuleSet_reorderRules() + { + List dbOrder = List.of("RuleA", "RuleB"); + List newOrder = List.of("RuleB", "RuleA"); + List includes = List.of(RULE_IDS); + + RuleSet dbRuleSet = mock(RuleSet.class); + RuleSet requestRuleSet = mock(RuleSet.class); + given(requestRuleSet.getId()).willReturn(RULE_SET_ID); + given(requestRuleSet.getRuleIds()).willReturn(newOrder); + + given(ruleSetLoaderMock.loadRuleSet(RULE_SET_NODE, FOLDER_NODE, includes)).willReturn(dbRuleSet); + given(ruleSetLoaderMock.loadRuleIds(FOLDER_NODE)).willReturn(dbOrder); + given(nodeValidatorMock.validateFolderNode(FOLDER_ID, false)).willReturn(FOLDER_NODE); + given(nodeValidatorMock.validateRuleSetNode(RULE_SET_ID, FOLDER_NODE)).willReturn(RULE_SET_NODE); + + //when + RuleSet ruleSet = ruleSets.updateRuleSet(FOLDER_ID, requestRuleSet, includes); + + assertEquals("Unexpected rule set returned.", dbRuleSet, ruleSet); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_ID, false); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE); + then(ruleSetLoaderMock).should().loadRuleSet(RULE_SET_NODE, FOLDER_NODE, includes); + then(dbRuleSet).should().setRuleIds(newOrder); + } + + /** Check that we can't remove a rule by updating the rule set. */ + @Test + public void testUpdateRuleSet_tryToChangeSetOfRuleIds() + { + List dbOrder = List.of("RuleA", "RuleB"); + List newOrder = List.of("RuleA"); + List includes = List.of(RULE_IDS); + + RuleSet dbRuleSet = mock(RuleSet.class); + RuleSet requestRuleSet = mock(RuleSet.class); + given(requestRuleSet.getId()).willReturn(RULE_SET_ID); + given(requestRuleSet.getRuleIds()).willReturn(newOrder); + + given(ruleSetLoaderMock.loadRuleSet(RULE_SET_NODE, FOLDER_NODE, includes)).willReturn(dbRuleSet); + given(ruleSetLoaderMock.loadRuleIds(FOLDER_NODE)).willReturn(dbOrder); + given(nodeValidatorMock.validateFolderNode(FOLDER_ID, false)).willReturn(FOLDER_NODE); + given(nodeValidatorMock.validateRuleSetNode(RULE_SET_ID, FOLDER_NODE)).willReturn(RULE_SET_NODE); + + //when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy( + () -> ruleSets.updateRuleSet(FOLDER_ID, requestRuleSet, includes) + ); + } + + /** Check that we can't include a rule twice in a rule set. */ + @Test + public void testUpdateRuleSet_DuplicateRuleId() + { + List dbOrder = List.of("RuleA", "RuleB"); + List newOrder = List.of("RuleA", "RuleB", "RuleA"); + List includes = List.of(RULE_IDS); + + RuleSet dbRuleSet = mock(RuleSet.class); + RuleSet requestRuleSet = mock(RuleSet.class); + given(requestRuleSet.getId()).willReturn(RULE_SET_ID); + given(requestRuleSet.getRuleIds()).willReturn(newOrder); + + given(ruleSetLoaderMock.loadRuleSet(RULE_SET_NODE, FOLDER_NODE, includes)).willReturn(dbRuleSet); + given(ruleSetLoaderMock.loadRuleIds(FOLDER_NODE)).willReturn(dbOrder); + given(nodeValidatorMock.validateFolderNode(FOLDER_ID, false)).willReturn(FOLDER_NODE); + given(nodeValidatorMock.validateRuleSetNode(RULE_SET_ID, FOLDER_NODE)).willReturn(RULE_SET_NODE); + + //when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy( + () -> ruleSets.updateRuleSet(FOLDER_ID, requestRuleSet, includes) + ); + } + + /** Check that we can update the rule ids without returning them. */ + @Test + public void testUpdateRuleSet_dontIncludeRuleIds() + { + List dbOrder = List.of("RuleA", "RuleB"); + List newOrder = List.of("RuleB", "RuleA"); + List includes = emptyList(); + + RuleSet dbRuleSet = mock(RuleSet.class); + RuleSet requestRuleSet = mock(RuleSet.class); + given(requestRuleSet.getId()).willReturn(RULE_SET_ID); + given(requestRuleSet.getRuleIds()).willReturn(newOrder); + + given(ruleSetLoaderMock.loadRuleSet(RULE_SET_NODE, FOLDER_NODE, includes)).willReturn(dbRuleSet); + given(ruleSetLoaderMock.loadRuleIds(FOLDER_NODE)).willReturn(dbOrder); + given(nodeValidatorMock.validateFolderNode(FOLDER_ID, false)).willReturn(FOLDER_NODE); + given(nodeValidatorMock.validateRuleSetNode(RULE_SET_ID, FOLDER_NODE)).willReturn(RULE_SET_NODE); + + //when + RuleSet ruleSet = ruleSets.updateRuleSet(FOLDER_ID, requestRuleSet, includes); + + // Expect the DB rule set to be returned, but no extra fields to be populated. + assertEquals("Unexpected rule set returned.", dbRuleSet, ruleSet); + then(dbRuleSet).shouldHaveNoInteractions(); + } +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSettingsImplTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSettingsImplTest.java new file mode 100644 index 0000000000..0f635a73c1 --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RuleSettingsImplTest.java @@ -0,0 +1,145 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.impl.rules; + +import static java.util.Collections.emptyMap; + +import static org.alfresco.repo.rule.RuleModel.ASPECT_IGNORE_INHERITED_RULES; +import static org.alfresco.rest.api.model.rules.RuleSetting.IS_INHERITANCE_ENABLED_KEY; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import junit.framework.TestCase; +import org.alfresco.rest.api.model.rules.RuleSetting; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** Unit tests for {@link RuleSettingsImpl}. */ +@Experimental +@RunWith (MockitoJUnitRunner.class) +public class RuleSettingsImplTest extends TestCase +{ + private static final String FOLDER_ID = "dummy-folder-id"; + private static final NodeRef FOLDER_NODE = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, FOLDER_ID); + + @InjectMocks + private RuleSettingsImpl ruleSettings; + @Mock + private NodeValidator nodeValidatorMock; + @Mock + private NodeService nodeServiceMock; + + @Before + @Override + public void setUp() + { + given(nodeValidatorMock.validateFolderNode(eq(FOLDER_ID), anyBoolean())).willReturn(FOLDER_NODE); + } + + @Test + public void testGetRuleSetting_disabled() + { + given(nodeServiceMock.hasAspect(FOLDER_NODE, ASPECT_IGNORE_INHERITED_RULES)).willReturn(true); + + // Call the method under test. + RuleSetting ruleSetting = ruleSettings.getRuleSetting(FOLDER_ID, IS_INHERITANCE_ENABLED_KEY); + + RuleSetting expected = RuleSetting.builder().key(IS_INHERITANCE_ENABLED_KEY).value(false).create(); + assertEquals(expected, ruleSetting); + } + + @Test + public void testGetRuleSetting_enabled() + { + given(nodeServiceMock.hasAspect(FOLDER_NODE, ASPECT_IGNORE_INHERITED_RULES)).willReturn(false); + + // Call the method under test. + RuleSetting ruleSetting = ruleSettings.getRuleSetting(FOLDER_ID, IS_INHERITANCE_ENABLED_KEY); + + RuleSetting expected = RuleSetting.builder().key(IS_INHERITANCE_ENABLED_KEY).value(true).create(); + assertEquals(expected, ruleSetting); + } + + @Test + public void testGetRuleSetting_unrecognisedKey() + { + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> ruleSettings.getRuleSetting(FOLDER_ID, "-fakeSetting-")); + } + + @Test + public void testSetRuleSetting_enable() + { + RuleSetting ruleSetting = RuleSetting.builder().key(IS_INHERITANCE_ENABLED_KEY).value(true).create(); + + // Call the method under test. + RuleSetting actual = ruleSettings.setRuleSetting(FOLDER_ID, ruleSetting); + + assertEquals(ruleSetting, actual); + then(nodeServiceMock).should().removeAspect(FOLDER_NODE, ASPECT_IGNORE_INHERITED_RULES); + } + + @Test + public void testSetRuleSetting_disable() + { + RuleSetting ruleSetting = RuleSetting.builder().key(IS_INHERITANCE_ENABLED_KEY).value(false).create(); + + // Call the method under test. + RuleSetting actual = ruleSettings.setRuleSetting(FOLDER_ID, ruleSetting); + + assertEquals(ruleSetting, actual); + then(nodeServiceMock).should().addAspect(FOLDER_NODE, ASPECT_IGNORE_INHERITED_RULES, emptyMap()); + } + + @Test + public void testSetRuleSetting_unrecognisedKey() + { + RuleSetting ruleSetting = RuleSetting.builder().key("-fakeSetting-").value(true).create(); + assertThatExceptionOfType(NotFoundException.class) + .isThrownBy(() -> ruleSettings.setRuleSetting(FOLDER_ID, ruleSetting)); + } + + @Test + public void testSetRuleSetting_nonBooleanValue() + { + RuleSetting ruleSetting = RuleSetting.builder().key(IS_INHERITANCE_ENABLED_KEY).value(123456).create(); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> ruleSettings.setRuleSetting(FOLDER_ID, ruleSetting)); + } +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RulesImplTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RulesImplTest.java new file mode 100644 index 0000000000..561b738d92 --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/rules/RulesImplTest.java @@ -0,0 +1,699 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.rules; + +import static java.util.Collections.emptyList; + +import static org.alfresco.rest.api.model.rules.RuleSet.DEFAULT_ID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +import junit.framework.TestCase; +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.repo.action.access.ActionAccessRestriction; +import org.alfresco.repo.action.executer.ExecuteAllRulesActionExecuter; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.mapper.RestModelMapper; +import org.alfresco.rest.api.model.rules.Action; +import org.alfresco.rest.api.model.rules.Rule; +import org.alfresco.rest.api.model.rules.RuleExecution; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.core.exceptions.RelationshipResourceNotFoundException; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Paging; +import org.alfresco.service.Experimental; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.rule.RuleService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@Experimental +@RunWith(MockitoJUnitRunner.class) +public class RulesImplTest extends TestCase +{ + private static final String FOLDER_NODE_ID = "dummy-folder-node-id"; + private static final String RULE_SET_ID = "dummy-rule-set-id"; + private static final String RULE_ID = "dummy-rule-id"; + private static final NodeRef FOLDER_NODE_REF = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, FOLDER_NODE_ID); + private static final NodeRef RULE_SET_NODE_REF = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, RULE_SET_ID); + private static final NodeRef RULE_NODE_REF = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, RULE_ID); + private static final Paging PAGING = Paging.DEFAULT; + private static final List INCLUDE = emptyList(); + private static final boolean INCLUDE_SUB_FOLDERS = true; + private static final boolean EXECUTE_INHERITED_RULES = true; + + @Mock + private Nodes nodesMock; + @Mock + private ActionService actionServiceMock; + @Mock + private RestModelMapper ruleMapper; + @Mock + private NodeValidator nodeValidatorMock; + @Mock + private RuleService ruleServiceMock; + @Mock + private RuleLoader ruleLoaderMock; + @Mock + private ActionPermissionValidator actionPermissionValidatorMock; + @Mock + private org.alfresco.service.cmr.rule.Rule serviceRuleMock; + @Mock + private Rule ruleMock; + @Mock + private Action actionMock; + + private org.alfresco.service.cmr.rule.Rule ruleModel = createRule(RULE_ID); + + @InjectMocks + private RulesImpl rules; + + @Before + @Override + public void setUp() throws Exception + { + given(nodeValidatorMock.validateFolderNode(any(), anyBoolean())).willReturn(FOLDER_NODE_REF); + given(nodeValidatorMock.validateRuleSetNode(any(), any())).willReturn(RULE_SET_NODE_REF); + given(nodeValidatorMock.validateRuleNode(any(), any())).willReturn(RULE_NODE_REF); + + given(ruleServiceMock.getRule(RULE_NODE_REF)).willReturn(ruleModel); + given(ruleServiceMock.getRules(FOLDER_NODE_REF, false)).willReturn(List.of(ruleModel)); + given(ruleServiceMock.getOwningNodeRef(RULE_SET_NODE_REF)).willReturn(FOLDER_NODE_REF); + + given(ruleLoaderMock.loadRule(ruleModel, INCLUDE)).willReturn(ruleMock); + } + + @Test + public void testGetRules() + { + given(ruleLoaderMock.loadRule(ruleModel, emptyList())).willReturn(ruleMock); + + // when + final CollectionWithPagingInfo rulesPage = rules.getRules(FOLDER_NODE_ID, RULE_SET_ID, INCLUDE, PAGING); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, false); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE_REF); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).should().getOwningNodeRef(RULE_SET_NODE_REF); + then(ruleServiceMock).should().getRules(FOLDER_NODE_REF, false); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + then(ruleLoaderMock).should().loadRule(ruleModel, emptyList()); + then(ruleLoaderMock).shouldHaveNoMoreInteractions(); + assertThat(rulesPage) + .isNotNull() + .extracting(CollectionWithPagingInfo::getCollection) + .isNotNull() + .extracting(Collection::size) + .isEqualTo(1); + assertThat(rulesPage.getCollection().stream().findFirst().get()).isEqualTo(ruleMock); + } + + @Test + public void testGetRules_emptyResult() + { + given(ruleServiceMock.getRules(FOLDER_NODE_REF, false)).willReturn(emptyList()); + + // when + final CollectionWithPagingInfo rulesPage = rules.getRules(FOLDER_NODE_ID, RULE_SET_ID, INCLUDE, PAGING); + + then(ruleServiceMock).should().getOwningNodeRef(RULE_SET_NODE_REF); + then(ruleServiceMock).should().getRules(FOLDER_NODE_REF, false); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + assertThat(rulesPage) + .isNotNull() + .extracting(CollectionWithPagingInfo::getCollection) + .isNotNull() + .extracting(Collection::isEmpty) + .isEqualTo(true); + } + + @Test + public void testGetRules_invalidFolder() + { + for (Exception exception : folderValidationExceptions()) + { + Mockito.reset(nodeValidatorMock); + given(nodeValidatorMock.validateFolderNode(any(), anyBoolean())).willThrow(exception); + + // when + assertThatExceptionOfType(exception.getClass()).isThrownBy( + () -> rules.getRules(FOLDER_NODE_ID, RULE_SET_ID, INCLUDE, PAGING)); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, false); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + } + + @Test + public void testGetRules_invalidRuleSet() + { + for (Exception exception : ruleSetValidationExceptions()) + { + Mockito.reset(nodeValidatorMock); + given(nodeValidatorMock.validateFolderNode(any(), anyBoolean())).willReturn(FOLDER_NODE_REF); + given(nodeValidatorMock.validateRuleSetNode(any(), any())).willThrow(exception); + + // when + assertThatExceptionOfType(exception.getClass()).isThrownBy( + () -> rules.getRules(FOLDER_NODE_ID, RULE_SET_ID, INCLUDE, PAGING)); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, false); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE_REF); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + } + + @Test + public void testGetRuleById() + { + // when + final Rule rule = rules.getRuleById(FOLDER_NODE_ID, RULE_SET_ID, RULE_ID, INCLUDE); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, false); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE_REF); + then(nodeValidatorMock).should().validateRuleNode(RULE_ID, RULE_SET_NODE_REF); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(nodesMock).shouldHaveNoInteractions(); + then(ruleServiceMock).should().getRule(RULE_NODE_REF); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + assertThat(rule).isEqualTo(ruleMock); + } + + @Test + public void testGetRuleById_invalidFolder() + { + for (Exception exception : folderValidationExceptions()) + { + Mockito.reset(nodeValidatorMock); + given(nodeValidatorMock.validateFolderNode(any(), anyBoolean())).willThrow(exception); + + // when + assertThatExceptionOfType(exception.getClass()).isThrownBy( + () -> rules.getRuleById(FOLDER_NODE_ID, RULE_SET_ID, RULE_ID, INCLUDE)); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, false); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + } + + @Test + public void testGetRuleById_invalidRuleSet() + { + for (Exception exception : ruleSetValidationExceptions()) + { + Mockito.reset(nodeValidatorMock); + given(nodeValidatorMock.validateFolderNode(any(), anyBoolean())).willReturn(FOLDER_NODE_REF); + given(nodeValidatorMock.validateRuleSetNode(any(), any())).willThrow(exception); + + // when + assertThatExceptionOfType(exception.getClass()).isThrownBy( + () -> rules.getRuleById(FOLDER_NODE_ID, RULE_SET_ID, RULE_ID, INCLUDE)); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, false); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE_REF); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + } + + @Test + public void testGetRuleById_invalidRule() + { + for (Exception exception : ruleValidationExceptions()) + { + Mockito.reset(nodeValidatorMock); + given(nodeValidatorMock.validateFolderNode(any(), anyBoolean())).willReturn(FOLDER_NODE_REF); + given(nodeValidatorMock.validateRuleSetNode(any(), any())).willReturn(RULE_SET_NODE_REF); + given(nodeValidatorMock.validateRuleNode(any(), any())).willThrow(exception); + + // when + assertThatExceptionOfType(exception.getClass()).isThrownBy( + () -> rules.getRuleById(FOLDER_NODE_ID, RULE_SET_ID, RULE_ID, INCLUDE)); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, false); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE_REF); + then(nodeValidatorMock).should().validateRuleNode(RULE_ID, RULE_SET_NODE_REF); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + } + + /** + * Create a single rule. + */ + @Test + public void testCreateRules() + { + List ruleList = List.of(ruleMock); + given(ruleMapper.toServiceModel(ruleMock)).willReturn(serviceRuleMock); + given(ruleMock.getActions()).willReturn(List.of(actionMock)); + given(ruleServiceMock.saveRule(FOLDER_NODE_REF, serviceRuleMock)).willAnswer(arg -> arg.getArguments()[1]); + given(ruleLoaderMock.loadRule(serviceRuleMock, INCLUDE)).willReturn(ruleMock); + given(actionPermissionValidatorMock.validateRulePermissions(any())).willAnswer(arg -> arg.getArguments()[0]); + + // when + List actual = rules.createRules(FOLDER_NODE_REF.getId(), RULE_SET_NODE_REF.getId(), ruleList, INCLUDE); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, true); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE_REF); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(actionPermissionValidatorMock).should().validateRulePermissions(serviceRuleMock); + then(actionPermissionValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).should().saveRule(FOLDER_NODE_REF, serviceRuleMock); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + List expected = List.of(ruleMock); + assertThat(actual).isEqualTo(expected); + } + + /** + * Check that when passing the default rule set then we don't perform any validation around the rule set node. + */ + @Test + public void testCreateRules_defaultRuleSet() + { + List ruleList = List.of(ruleMock); + given(ruleMapper.toServiceModel(ruleMock)).willReturn(serviceRuleMock); + given(ruleMock.getActions()).willReturn(List.of(actionMock)); + given(ruleServiceMock.saveRule(FOLDER_NODE_REF, serviceRuleMock)).willAnswer(arg -> arg.getArguments()[1]); + given(ruleLoaderMock.loadRule(serviceRuleMock, INCLUDE)).willReturn(ruleMock); + given(actionPermissionValidatorMock.validateRulePermissions(any())).willAnswer(arg -> arg.getArguments()[0]); + + // when + List actual = rules.createRules(FOLDER_NODE_REF.getId(), DEFAULT_ID, ruleList, INCLUDE); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, true); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(actionPermissionValidatorMock).should().validateRulePermissions(serviceRuleMock); + then(actionPermissionValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).should().saveRule(FOLDER_NODE_REF, serviceRuleMock); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + List expected = List.of(ruleMock); + assertThat(actual).isEqualTo(expected); + } + + @Test + public void testCreateRules_emptyRuleList() + { + List ruleList = emptyList(); + + // when + List actual = rules.createRules(FOLDER_NODE_REF.getId(), RULE_SET_NODE_REF.getId(), ruleList, INCLUDE); + + then(ruleServiceMock).shouldHaveNoInteractions(); + assertThat(actual).isEqualTo(emptyList()); + } + + /** + * Create three rules in a single call and check they are all passed to the RuleService. + */ + @Test + public void testCreateRules_createMultipleRules() + { + List ruleBodyList = new ArrayList<>(); + List expected = new ArrayList<>(); + IntStream.range(0, 3).forEach(i -> { + Rule ruleBodyMock = mock(Rule.class); + given(ruleBodyMock.getActions()).willReturn(List.of(actionMock)); + ruleBodyList.add(ruleBodyMock); + org.alfresco.service.cmr.rule.Rule serviceRuleMockInner = mock(org.alfresco.service.cmr.rule.Rule.class); + given(ruleMapper.toServiceModel(ruleBodyMock)).willReturn(serviceRuleMockInner); + given(ruleServiceMock.saveRule(FOLDER_NODE_REF, serviceRuleMockInner)).willAnswer(arg -> arg.getArguments()[1]); + Rule ruleMockInner = mock(Rule.class); + given(ruleLoaderMock.loadRule(serviceRuleMockInner, INCLUDE)).willReturn(ruleMockInner); + expected.add(ruleMockInner); + given(actionPermissionValidatorMock.validateRulePermissions(any())).willAnswer(arg -> arg.getArguments()[0]); + }); + + // when + List actual = rules.createRules(FOLDER_NODE_REF.getId(), RULE_SET_NODE_REF.getId(), ruleBodyList, INCLUDE); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, true); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE_REF); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + for (Rule ruleBody : ruleBodyList) + { + then(actionPermissionValidatorMock).should().validateRulePermissions(ruleMapper.toServiceModel(ruleBody)); + then(ruleServiceMock).should().saveRule(FOLDER_NODE_REF, ruleMapper.toServiceModel(ruleBody)); + } + then(actionPermissionValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + assertThat(actual).isEqualTo(expected); + } + + @Test + public void testCreateRules_invalidFolder() + { + for (Exception exception : folderValidationExceptions()) + { + Mockito.reset(nodeValidatorMock); + given(nodeValidatorMock.validateFolderNode(any(), anyBoolean())).willThrow(exception); + + // when + assertThatExceptionOfType(exception.getClass()).isThrownBy( + () -> rules.createRules(FOLDER_NODE_REF.getId(), RULE_SET_NODE_REF.getId(), emptyList(), INCLUDE)); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, true); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + } + + @Test + public void testCreateRules_invalidRuleSet() + { + for (Exception exception : ruleSetValidationExceptions()) + { + Mockito.reset(nodeValidatorMock); + given(nodeValidatorMock.validateFolderNode(any(), anyBoolean())).willReturn(FOLDER_NODE_REF); + given(nodeValidatorMock.validateRuleSetNode(any(), any())).willThrow(exception); + + // when + assertThatExceptionOfType(exception.getClass()).isThrownBy( + () -> rules.createRules(FOLDER_NODE_REF.getId(), RULE_SET_NODE_REF.getId(), emptyList(), INCLUDE)); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, true); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE_REF); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + } + + /** + * Fail on create a rule without any actions. + */ + @Test + public void testCreateRuleWithoutActionsShouldFail() + { + List ruleList = List.of(ruleMock); + given(ruleMock.getActions()).willReturn(null); + + // when + assertThatExceptionOfType(InvalidArgumentException.class) + .isThrownBy(() -> rules.createRules(FOLDER_NODE_REF.getId(), RULE_SET_NODE_REF.getId(), ruleList, INCLUDE)); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, true); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE_REF); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(actionPermissionValidatorMock).shouldHaveNoInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + + /** + * Check that we can update a rule. + */ + @Test + public void testUpdateRuleById() + { + given(ruleMapper.toServiceModel(ruleMock)).willReturn(serviceRuleMock); + given(ruleMock.getActions()).willReturn(List.of(actionMock)); + given(ruleServiceMock.saveRule(FOLDER_NODE_REF, serviceRuleMock)).willAnswer(a -> a.getArguments()[1]); + given(ruleLoaderMock.loadRule(serviceRuleMock, INCLUDE)).willReturn(ruleMock); + given(actionPermissionValidatorMock.validateRulePermissions(any())).willAnswer(arg -> arg.getArguments()[0]); + + // when + Rule updatedRule = rules.updateRuleById(FOLDER_NODE_REF.getId(), RULE_SET_NODE_REF.getId(), RULE_ID, ruleMock, INCLUDE); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, true); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE_REF); + then(nodeValidatorMock).should().validateRuleNode(RULE_ID, RULE_SET_NODE_REF); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).should().saveRule(FOLDER_NODE_REF, serviceRuleMock); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + then(actionPermissionValidatorMock).should().validateRulePermissions(serviceRuleMock); + then(actionPermissionValidatorMock).shouldHaveNoMoreInteractions(); + assertThat(updatedRule).isEqualTo(ruleMock); + } + + @Test + public void testUpdateRuleById_invalidFolder() + { + for (Exception exception : folderValidationExceptions()) + { + Mockito.reset(nodeValidatorMock); + given(nodeValidatorMock.validateFolderNode(any(), anyBoolean())).willThrow(exception); + + // when + assertThatExceptionOfType(exception.getClass()).isThrownBy( + () -> rules.updateRuleById(FOLDER_NODE_REF.getId(), RULE_SET_NODE_REF.getId(), RULE_ID, mock(Rule.class), INCLUDE)); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, true); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + } + + @Test + public void testUpdateRuleById_invalidRuleSet() + { + for (Exception exception : ruleSetValidationExceptions()) + { + Mockito.reset(nodeValidatorMock); + given(nodeValidatorMock.validateFolderNode(any(), anyBoolean())).willReturn(FOLDER_NODE_REF); + given(nodeValidatorMock.validateRuleSetNode(any(), any())).willThrow(exception); + + // when + assertThatExceptionOfType(exception.getClass()).isThrownBy( + () -> rules.updateRuleById(FOLDER_NODE_REF.getId(), RULE_SET_NODE_REF.getId(), RULE_ID, mock(Rule.class), INCLUDE)); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, true); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE_REF); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + } + + @Test + public void testUpdateRuleById_invalidRule() + { + for (Exception exception : ruleValidationExceptions()) + { + Mockito.reset(nodeValidatorMock); + given(nodeValidatorMock.validateFolderNode(any(), anyBoolean())).willReturn(FOLDER_NODE_REF); + given(nodeValidatorMock.validateRuleSetNode(any(), any())).willReturn(RULE_SET_NODE_REF); + given(nodeValidatorMock.validateRuleNode(any(), any())).willThrow(exception); + + // when + assertThatExceptionOfType(exception.getClass()).isThrownBy( + () -> rules.updateRuleById(FOLDER_NODE_REF.getId(), RULE_SET_NODE_REF.getId(), RULE_ID, mock(Rule.class), INCLUDE)); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, true); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE_REF); + then(nodeValidatorMock).should().validateRuleNode(RULE_ID, RULE_SET_NODE_REF); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + } + + /** + * Fail on update a rule without any actions. + */ + @Test + public void testUpdateRuleWithoutActionShouldFail() + { + given(ruleMock.getActions()).willReturn(emptyList()); + + // when + assertThatExceptionOfType(InvalidArgumentException.class) + .isThrownBy(() -> rules.updateRuleById(FOLDER_NODE_REF.getId(), RULE_SET_NODE_REF.getId(), RULE_ID, ruleMock, INCLUDE)); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, true); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE_REF); + then(nodeValidatorMock).should().validateRuleNode(RULE_ID, RULE_SET_NODE_REF); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + + then(ruleServiceMock).shouldHaveNoInteractions(); + then(actionPermissionValidatorMock).shouldHaveNoInteractions(); + } + + @Test + public void testDeleteRuleById() + { + //when + rules.deleteRuleById(FOLDER_NODE_ID, RULE_SET_ID, RULE_ID); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, true); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE_REF); + then(nodeValidatorMock).should().validateRuleNode(RULE_ID, RULE_SET_NODE_REF); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(nodesMock).shouldHaveNoInteractions(); + then(ruleServiceMock).should().getRule(RULE_NODE_REF); + then(ruleServiceMock).should().removeRule(FOLDER_NODE_REF, ruleModel); + then(ruleServiceMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testDeleteRuleById_invalidFolder() + { + for (Exception exception : folderValidationExceptions()) + { + Mockito.reset(nodeValidatorMock); + given(nodeValidatorMock.validateFolderNode(any(), anyBoolean())).willThrow(exception); + + // when + assertThatExceptionOfType(exception.getClass()).isThrownBy( + () -> rules.deleteRuleById(FOLDER_NODE_ID, RULE_SET_ID, RULE_ID)); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, true); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + } + + @Test + public void testDeleteRuleById_invalidRuleSet() + { + for (Exception exception : ruleSetValidationExceptions()) + { + Mockito.reset(nodeValidatorMock); + given(nodeValidatorMock.validateFolderNode(any(), anyBoolean())).willReturn(FOLDER_NODE_REF); + given(nodeValidatorMock.validateRuleSetNode(any(), any())).willThrow(exception); + + // when + assertThatExceptionOfType(exception.getClass()).isThrownBy( + () -> rules.deleteRuleById(FOLDER_NODE_ID, RULE_SET_ID, RULE_ID)); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, true); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE_REF); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + } + + @Test + public void testDeleteRuleById_invalidRule() + { + for (Exception exception : ruleValidationExceptions()) + { + Mockito.reset(nodeValidatorMock); + given(nodeValidatorMock.validateFolderNode(any(), anyBoolean())).willReturn(FOLDER_NODE_REF); + given(nodeValidatorMock.validateRuleSetNode(any(), any())).willReturn(RULE_SET_NODE_REF); + given(nodeValidatorMock.validateRuleNode(any(), any())).willThrow(exception); + + // when + assertThatExceptionOfType(exception.getClass()).isThrownBy( + () -> rules.deleteRuleById(FOLDER_NODE_ID, RULE_SET_ID, RULE_ID)); + + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, true); + then(nodeValidatorMock).should().validateRuleSetNode(RULE_SET_ID, FOLDER_NODE_REF); + then(nodeValidatorMock).should().validateRuleNode(RULE_ID, RULE_SET_NODE_REF); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(ruleServiceMock).shouldHaveNoInteractions(); + } + } + + @Test + public void testExecuteRule() + { + // when + final RuleExecution actualRuleExecution = rules.executeRules(FOLDER_NODE_ID, INCLUDE_SUB_FOLDERS); + + final RuleExecution expectedRuleExecution = RuleExecution.builder().eachSubFolderIncluded(INCLUDE_SUB_FOLDERS).create(); + final ActionImpl expectedAction = new ActionImpl(null, null, ExecuteAllRulesActionExecuter.NAME); + expectedAction.setNodeRef(FOLDER_NODE_REF); + expectedAction.setParameterValues(Map.of( + ExecuteAllRulesActionExecuter.PARAM_RUN_ALL_RULES_ON_CHILDREN, INCLUDE_SUB_FOLDERS, + ExecuteAllRulesActionExecuter.PARAM_EXECUTE_INHERITED_RULES, EXECUTE_INHERITED_RULES, + ActionAccessRestriction.ACTION_CONTEXT_PARAM_NAME, ActionAccessRestriction.V1_ACTION_CONTEXT) + ); + final ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(ActionImpl.class); + then(nodeValidatorMock).should().validateFolderNode(FOLDER_NODE_ID, false); + then(nodeValidatorMock).shouldHaveNoMoreInteractions(); + then(actionServiceMock).should().executeAction(actionCaptor.capture(), eq(FOLDER_NODE_REF), eq(true), eq(false)); + then(actionServiceMock).shouldHaveNoMoreInteractions(); + final ActionImpl actualAction = actionCaptor.getValue(); + assertThat(actualAction) + .isNotNull() + .usingRecursiveComparison().ignoringFields("id") + .isEqualTo(expectedAction); + assertThat(actualRuleExecution) + .isNotNull() + .usingRecursiveComparison() + .isEqualTo(expectedRuleExecution); + } + + private static org.alfresco.service.cmr.rule.Rule createRule(final String id) + { + final NodeRef nodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, id); + final org.alfresco.service.cmr.rule.Rule rule = new org.alfresco.service.cmr.rule.Rule(); + rule.setNodeRef(nodeRef); + rule.setRuleType("ruleType"); + + return rule; + } + + private static List folderValidationExceptions() + { + return List.of( + new EntityNotFoundException(FOLDER_NODE_ID), + new InvalidArgumentException(), + new PermissionDeniedException() + ); + } + + private static List ruleSetValidationExceptions() + { + return List.of( + new EntityNotFoundException(RULE_SET_ID), + new InvalidArgumentException(), + new RelationshipResourceNotFoundException(RULE_SET_ID, "fake-relationship-id") + ); + } + + private static List ruleValidationExceptions() + { + return List.of( + new EntityNotFoundException(RULE_ID), + new InvalidArgumentException(), + new RelationshipResourceNotFoundException(RULE_ID, "fake-relationship-id") + ); + } +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/validator/actions/ActionNodeParameterValidatorTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/validator/actions/ActionNodeParameterValidatorTest.java new file mode 100644 index 0000000000..047d2da7e1 --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/validator/actions/ActionNodeParameterValidatorTest.java @@ -0,0 +1,366 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.validator.actions; + +import static org.alfresco.model.ContentModel.TYPE_CATEGORY; +import static org.alfresco.model.ContentModel.TYPE_FOLDER; +import static org.alfresco.rest.api.impl.validator.actions.ActionNodeParameterValidator.NOT_A_CATEGORY; +import static org.alfresco.rest.api.impl.validator.actions.ActionNodeParameterValidator.NOT_A_FOLDER; +import static org.alfresco.rest.api.impl.validator.actions.ActionNodeParameterValidator.NO_PROPER_PERMISSIONS_FOR_NODE; +import static org.alfresco.rest.api.impl.validator.actions.ActionNodeParameterValidator.REQUIRE_READ_PERMISSION_PARAMS; +import static org.alfresco.service.cmr.dictionary.DataTypeDefinition.CATEGORY; +import static org.alfresco.service.cmr.dictionary.DataTypeDefinition.NODE_REF; +import static org.alfresco.service.cmr.dictionary.DataTypeDefinition.TEXT; +import static org.alfresco.service.cmr.repository.StoreRef.STORE_REF_WORKSPACE_SPACESSTORE; +import static org.alfresco.service.namespace.NamespaceService.DEFAULT_PREFIX; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.Assert.assertEquals; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.alfresco.repo.action.executer.CheckOutActionExecuter; +import org.alfresco.repo.action.executer.CopyActionExecuter; +import org.alfresco.repo.action.executer.ImageTransformActionExecuter; +import org.alfresco.repo.action.executer.ImporterActionExecuter; +import org.alfresco.repo.action.executer.LinkCategoryActionExecuter; +import org.alfresco.repo.action.executer.MoveActionExecuter; +import org.alfresco.repo.action.executer.SimpleWorkflowActionExecuter; +import org.alfresco.repo.action.executer.TransformActionExecuter; +import org.alfresco.rest.api.Actions; +import org.alfresco.rest.api.Nodes; +import org.alfresco.rest.api.model.ActionDefinition; +import org.alfresco.rest.api.model.rules.Action; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.NamespaceService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ActionNodeParameterValidatorTest +{ + private static final String READ_RIGHTS_REQUIRED_DEFINITION_ID = LinkCategoryActionExecuter.NAME; + private static final String CATEGORY_NODE_REF_PARAM = REQUIRE_READ_PERMISSION_PARAMS.get(READ_RIGHTS_REQUIRED_DEFINITION_ID).get(0); + private static final String DESTINATION_FOLDER_PARAM = "destination-folder"; + private static final String NODE_ID = "node-id"; + private static final String COPY_ACTION = CopyActionExecuter.NAME; + + @Mock + private Actions actionsMock; + @Mock + private NamespaceService namespaceServiceMock; + @Mock + private Nodes nodesMock; + @Mock + private PermissionService permissionServiceMock; + + @InjectMocks + private ActionNodeParameterValidator objectUnderTest; + + @Test + public void testProperPermissionsForReadRights() + { + final Action action = new Action(); + action.setActionDefinitionId(READ_RIGHTS_REQUIRED_DEFINITION_ID); + action.setParams(Map.of(CATEGORY_NODE_REF_PARAM, NODE_ID)); + ActionDefinition.ParameterDefinition parameterDef = + new ActionDefinition.ParameterDefinition(CATEGORY_NODE_REF_PARAM, NODE_REF.toPrefixString(), false, true, null, null); + final ActionDefinition actionDefinition = + new ActionDefinition(COPY_ACTION, COPY_ACTION, null, null, null, false, false, + List.of(parameterDef)); + given(actionsMock.getRuleActionDefinitionById(READ_RIGHTS_REQUIRED_DEFINITION_ID)).willReturn(actionDefinition); + given(namespaceServiceMock.getPrefixes(NODE_REF.getNamespaceURI())).willReturn(List.of(DEFAULT_PREFIX)); + final NodeRef nodeRef = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, NODE_ID); + given(nodesMock.validateNode(NODE_ID)).willReturn(nodeRef); + given(permissionServiceMock.hasReadPermission(nodeRef)).willReturn(AccessStatus.ALLOWED); + given(nodesMock.nodeMatches(nodeRef, Set.of(TYPE_CATEGORY), Collections.emptySet())).willReturn(true); + + //when + objectUnderTest.validate(action); + + then(actionsMock).should().getRuleActionDefinitionById(READ_RIGHTS_REQUIRED_DEFINITION_ID); + then(actionsMock).shouldHaveNoMoreInteractions(); + then(namespaceServiceMock).should().getPrefixes(NODE_REF.getNamespaceURI()); + then(namespaceServiceMock).shouldHaveNoMoreInteractions(); + then(nodesMock).should().validateNode(NODE_ID); + then(nodesMock).should().nodeMatches(nodeRef, Set.of(TYPE_CATEGORY), Collections.emptySet()); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(permissionServiceMock).should().hasReadPermission(nodeRef); + then(permissionServiceMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testNotEnoughPermissionsForReadRights() + { + final Action action = new Action(); + action.setActionDefinitionId(COPY_ACTION); + action.setParams(Map.of(DESTINATION_FOLDER_PARAM, NODE_ID)); + ActionDefinition.ParameterDefinition parameterDef = + new ActionDefinition.ParameterDefinition(DESTINATION_FOLDER_PARAM, NODE_REF.toPrefixString(), false, true, null, null); + final ActionDefinition actionDefinition = + new ActionDefinition(COPY_ACTION, COPY_ACTION, null, null, null, false, false, + List.of(parameterDef)); + given(actionsMock.getRuleActionDefinitionById(COPY_ACTION)).willReturn(actionDefinition); + given(namespaceServiceMock.getPrefixes(NODE_REF.getNamespaceURI())).willReturn(List.of(DEFAULT_PREFIX)); + final NodeRef nodeRef = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, NODE_ID); + given(nodesMock.validateNode(NODE_ID)).willReturn(nodeRef); + given(permissionServiceMock.hasReadPermission(nodeRef)).willReturn(AccessStatus.DENIED); + + //when + assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> objectUnderTest.validate(action)); + + then(actionsMock).should().getRuleActionDefinitionById(COPY_ACTION); + then(actionsMock).shouldHaveNoMoreInteractions(); + then(namespaceServiceMock).should().getPrefixes(NODE_REF.getNamespaceURI()); + then(namespaceServiceMock).shouldHaveNoMoreInteractions(); + then(nodesMock).should().validateNode(NODE_ID); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(permissionServiceMock).should().hasReadPermission(nodeRef); + then(permissionServiceMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testValidateForNodeNotFound() + { + final Action action = new Action(); + action.setActionDefinitionId(COPY_ACTION); + action.setParams(Map.of(DESTINATION_FOLDER_PARAM, NODE_ID)); + ActionDefinition.ParameterDefinition parameterDef = + new ActionDefinition.ParameterDefinition(DESTINATION_FOLDER_PARAM, NODE_REF.toPrefixString(), false, true, null, null); + final ActionDefinition actionDefinition = + new ActionDefinition(COPY_ACTION, COPY_ACTION, null, null, null, false, false, + List.of(parameterDef)); + given(actionsMock.getRuleActionDefinitionById(COPY_ACTION)).willReturn(actionDefinition); + given(namespaceServiceMock.getPrefixes(NODE_REF.getNamespaceURI())).willReturn(List.of(DEFAULT_PREFIX)); + given(nodesMock.validateNode(NODE_ID)).willThrow(EntityNotFoundException.class); + + //when + assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> objectUnderTest.validate(action)); + + then(actionsMock).should().getRuleActionDefinitionById(COPY_ACTION); + then(actionsMock).shouldHaveNoMoreInteractions(); + then(namespaceServiceMock).should().getPrefixes(NODE_REF.getNamespaceURI()); + then(namespaceServiceMock).shouldHaveNoMoreInteractions(); + then(nodesMock).should().validateNode(NODE_ID); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(permissionServiceMock).shouldHaveNoInteractions(); + } + + @Test + public void testProperPermissionsForWriteRights() + { + final Action action = new Action(); + action.setActionDefinitionId(COPY_ACTION); + action.setParams(Map.of(DESTINATION_FOLDER_PARAM, NODE_ID)); + ActionDefinition.ParameterDefinition parameterDef = + new ActionDefinition.ParameterDefinition(DESTINATION_FOLDER_PARAM, NODE_REF.toPrefixString(), false, true, null, null); + final ActionDefinition actionDefinition = + new ActionDefinition(COPY_ACTION, COPY_ACTION, null, null, null, false, false, + List.of(parameterDef)); + given(actionsMock.getRuleActionDefinitionById(COPY_ACTION)).willReturn(actionDefinition); + given(namespaceServiceMock.getPrefixes(NODE_REF.getNamespaceURI())).willReturn(List.of(DEFAULT_PREFIX)); + final NodeRef nodeRef = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, NODE_ID); + given(nodesMock.validateNode(NODE_ID)).willReturn(nodeRef); + given(permissionServiceMock.hasReadPermission(nodeRef)).willReturn(AccessStatus.ALLOWED); + given(permissionServiceMock.hasPermission(nodeRef, PermissionService.WRITE)).willReturn(AccessStatus.ALLOWED); + given(nodesMock.nodeMatches(nodeRef, Set.of(TYPE_FOLDER), Collections.emptySet())).willReturn(true); + + //when + objectUnderTest.validate(action); + + then(actionsMock).should().getRuleActionDefinitionById(COPY_ACTION); + then(actionsMock).shouldHaveNoMoreInteractions(); + then(namespaceServiceMock).should().getPrefixes(NODE_REF.getNamespaceURI()); + then(namespaceServiceMock).shouldHaveNoMoreInteractions(); + then(nodesMock).should().validateNode(NODE_ID); + then(nodesMock).should().nodeMatches(nodeRef, Set.of(TYPE_FOLDER), Collections.emptySet()); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(permissionServiceMock).should().hasReadPermission(nodeRef); + then(permissionServiceMock).should().hasPermission(nodeRef, PermissionService.WRITE); + then(permissionServiceMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testNotEnoughPermissionsForWriteRights() + { + final Action action = new Action(); + action.setActionDefinitionId(COPY_ACTION); + action.setParams(Map.of(DESTINATION_FOLDER_PARAM, NODE_ID)); + ActionDefinition.ParameterDefinition parameterDef = + new ActionDefinition.ParameterDefinition(DESTINATION_FOLDER_PARAM, NODE_REF.toPrefixString(), false, true, null, null); + final ActionDefinition actionDefinition = + new ActionDefinition(COPY_ACTION, COPY_ACTION, null, null, null, false, false, + List.of(parameterDef)); + given(actionsMock.getRuleActionDefinitionById(COPY_ACTION)).willReturn(actionDefinition); + given(namespaceServiceMock.getPrefixes(NODE_REF.getNamespaceURI())).willReturn(List.of(DEFAULT_PREFIX)); + final NodeRef nodeRef = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, NODE_ID); + given(nodesMock.validateNode(NODE_ID)).willReturn(nodeRef); + given(permissionServiceMock.hasReadPermission(nodeRef)).willReturn(AccessStatus.ALLOWED); + given(permissionServiceMock.hasPermission(nodeRef, PermissionService.WRITE)).willReturn(AccessStatus.DENIED); + + //when + assertThatExceptionOfType(PermissionDeniedException.class).isThrownBy(() -> objectUnderTest.validate(action)) + .withMessageContaining(NO_PROPER_PERMISSIONS_FOR_NODE + NODE_ID); + + then(actionsMock).should().getRuleActionDefinitionById(COPY_ACTION); + then(actionsMock).shouldHaveNoMoreInteractions(); + then(namespaceServiceMock).should().getPrefixes(NODE_REF.getNamespaceURI()); + then(namespaceServiceMock).shouldHaveNoMoreInteractions(); + then(nodesMock).should().validateNode(NODE_ID); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(permissionServiceMock).should().hasReadPermission(nodeRef); + then(permissionServiceMock).should().hasPermission(nodeRef, PermissionService.WRITE); + then(permissionServiceMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testNoValidationExecutedForNonNodeRefParam() + { + final Action action = new Action(); + action.setActionDefinitionId(COPY_ACTION); + final String dummyParam = "dummyParam"; + action.setParams(Map.of(dummyParam, "dummyValue")); + ActionDefinition.ParameterDefinition parameterDef = + new ActionDefinition.ParameterDefinition(dummyParam, TEXT.toPrefixString(), false, true, null, null); + final ActionDefinition actionDefinition = + new ActionDefinition(COPY_ACTION, COPY_ACTION, null, null, null, false, false, + List.of(parameterDef)); + given(actionsMock.getRuleActionDefinitionById(COPY_ACTION)).willReturn(actionDefinition); + given(namespaceServiceMock.getPrefixes(NODE_REF.getNamespaceURI())).willReturn(List.of(DEFAULT_PREFIX)); + + //when + objectUnderTest.validate(action); + + then(actionsMock).should().getRuleActionDefinitionById(COPY_ACTION); + then(actionsMock).shouldHaveNoMoreInteractions(); + then(namespaceServiceMock).should().getPrefixes(NODE_REF.getNamespaceURI()); + then(namespaceServiceMock).shouldHaveNoMoreInteractions(); + then(nodesMock).shouldHaveNoInteractions(); + then(permissionServiceMock).shouldHaveNoInteractions(); + } + + @Test + public void testWrongTypeOfNodeWhenFolderExpected() + { + final Action action = new Action(); + action.setActionDefinitionId(COPY_ACTION); + action.setParams(Map.of(DESTINATION_FOLDER_PARAM, NODE_ID)); + ActionDefinition.ParameterDefinition parameterDef = + new ActionDefinition.ParameterDefinition(DESTINATION_FOLDER_PARAM, NODE_REF.toPrefixString(), false, true, null, null); + final ActionDefinition actionDefinition = + new ActionDefinition(COPY_ACTION, COPY_ACTION, null, null, null, false, false, + List.of(parameterDef)); + given(actionsMock.getRuleActionDefinitionById(COPY_ACTION)).willReturn(actionDefinition); + given(namespaceServiceMock.getPrefixes(NODE_REF.getNamespaceURI())).willReturn(List.of(DEFAULT_PREFIX)); + final NodeRef nodeRef = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, NODE_ID); + given(nodesMock.validateNode(NODE_ID)).willReturn(nodeRef); + given(permissionServiceMock.hasReadPermission(nodeRef)).willReturn(AccessStatus.ALLOWED); + given(permissionServiceMock.hasPermission(nodeRef, PermissionService.WRITE)).willReturn(AccessStatus.ALLOWED); + given(nodesMock.nodeMatches(nodeRef, Set.of(TYPE_FOLDER), Collections.emptySet())).willReturn(false); + + //when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy(() -> objectUnderTest.validate(action)) + .withMessageContaining(NOT_A_FOLDER + NODE_ID); + + then(actionsMock).should().getRuleActionDefinitionById(COPY_ACTION); + then(actionsMock).shouldHaveNoMoreInteractions(); + then(namespaceServiceMock).should().getPrefixes(NODE_REF.getNamespaceURI()); + then(namespaceServiceMock).shouldHaveNoMoreInteractions(); + then(nodesMock).should().validateNode(NODE_ID); + then(nodesMock).should().nodeMatches(nodeRef, Set.of(TYPE_FOLDER), Collections.emptySet()); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(permissionServiceMock).should().hasReadPermission(nodeRef); + then(permissionServiceMock).should().hasPermission(nodeRef, PermissionService.WRITE); + then(permissionServiceMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testWrongTypeOfNodeWhenCategoryExpected() + { + final Action action = new Action(); + action.setActionDefinitionId(READ_RIGHTS_REQUIRED_DEFINITION_ID); + action.setParams(Map.of(CATEGORY_NODE_REF_PARAM, NODE_ID)); + ActionDefinition.ParameterDefinition parameterDef = + new ActionDefinition.ParameterDefinition(CATEGORY_NODE_REF_PARAM, NODE_REF.toPrefixString(), false, true, null, null); + final ActionDefinition actionDefinition = + new ActionDefinition(READ_RIGHTS_REQUIRED_DEFINITION_ID, READ_RIGHTS_REQUIRED_DEFINITION_ID, null, null, null, false, false, + List.of(parameterDef)); + given(actionsMock.getRuleActionDefinitionById(READ_RIGHTS_REQUIRED_DEFINITION_ID)).willReturn(actionDefinition); + given(namespaceServiceMock.getPrefixes(NODE_REF.getNamespaceURI())).willReturn(List.of(DEFAULT_PREFIX)); + final NodeRef nodeRef = new NodeRef(STORE_REF_WORKSPACE_SPACESSTORE, NODE_ID); + given(nodesMock.validateNode(NODE_ID)).willReturn(nodeRef); + given(permissionServiceMock.hasReadPermission(nodeRef)).willReturn(AccessStatus.ALLOWED); + given(nodesMock.nodeMatches(nodeRef, Set.of(TYPE_CATEGORY), Collections.emptySet())).willReturn(false); + + //when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy(() -> objectUnderTest.validate(action)) + .withMessageContaining(NOT_A_CATEGORY + NODE_ID); + + then(actionsMock).should().getRuleActionDefinitionById(READ_RIGHTS_REQUIRED_DEFINITION_ID); + then(actionsMock).shouldHaveNoMoreInteractions(); + then(namespaceServiceMock).should().getPrefixes(NODE_REF.getNamespaceURI()); + then(namespaceServiceMock).shouldHaveNoMoreInteractions(); + then(nodesMock).should().validateNode(NODE_ID); + then(nodesMock).should().nodeMatches(nodeRef, Set.of(TYPE_CATEGORY), Collections.emptySet()); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(permissionServiceMock).should().hasReadPermission(nodeRef); + then(permissionServiceMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testGetDefinitionIds() + { + final List expectedIds = + List.of(CopyActionExecuter.NAME, MoveActionExecuter.NAME, CheckOutActionExecuter.NAME, ImporterActionExecuter.NAME, + LinkCategoryActionExecuter.NAME, SimpleWorkflowActionExecuter.NAME, TransformActionExecuter.NAME, + ImageTransformActionExecuter.NAME); + final List actualIds = objectUnderTest.getActionDefinitionIds(); + + assertEquals(expectedIds, actualIds); + } + + @Test + public void testHasProperPriority() + { + final int expectedPriority = Integer.MIN_VALUE + 1; + final int actualPriority = objectUnderTest.getPriority(); + + assertEquals(expectedPriority, actualPriority); + } +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/validator/actions/ActionParameterDefinitionValidatorTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/validator/actions/ActionParameterDefinitionValidatorTest.java new file mode 100644 index 0000000000..8a87735684 --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/validator/actions/ActionParameterDefinitionValidatorTest.java @@ -0,0 +1,279 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.impl.validator.actions; + +import static org.alfresco.rest.api.impl.validator.actions.ActionParameterDefinitionValidator.EMPTY_ACTION_DEFINITION; +import static org.alfresco.rest.api.impl.validator.actions.ActionParameterDefinitionValidator.INVALID_ACTION_DEFINITION; +import static org.alfresco.rest.api.impl.validator.actions.ActionParameterDefinitionValidator.MISSING_PARAMETER; +import static org.alfresco.rest.api.impl.validator.actions.ActionParameterDefinitionValidator.MUST_NOT_CONTAIN_PARAMETER; +import static org.alfresco.rest.api.impl.validator.actions.ActionParameterDefinitionValidator.PARAMS_SHOULD_NOT_BE_EMPTY; +import static org.alfresco.service.cmr.dictionary.DataTypeDefinition.BOOLEAN; +import static org.alfresco.service.cmr.dictionary.DataTypeDefinition.TEXT; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.Assert.assertEquals; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.rest.api.Actions; +import org.alfresco.rest.api.model.ActionDefinition; +import org.alfresco.rest.api.model.rules.Action; +import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; +import org.alfresco.rest.framework.core.exceptions.NotFoundException; +import org.alfresco.service.Experimental; +import org.alfresco.service.namespace.QName; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@Experimental +@RunWith(MockitoJUnitRunner.class) +public class ActionParameterDefinitionValidatorTest +{ + private static final String MANDATORY_PARAM_KEY = "paramKey"; + private static final String NON_MANDATORY_PARAM_KEY = "nonMandatoryParamKey"; + + @Mock + private Actions actionsMock; + + @InjectMocks + private ActionParameterDefinitionValidator objectUnderTest; + + @Test + public void testSimpleValidationPasses() + { + final Action action = new Action(); + final String actionDefinitionId = "properActionDefinition"; + action.setActionDefinitionId(actionDefinitionId); + action.setParams(Map.of(MANDATORY_PARAM_KEY, "paramValue")); + final List parameterDefinitions = + List.of(createParameterDefinition(MANDATORY_PARAM_KEY, TEXT, true, null)); + final ActionDefinition actionDefinition = createActionDefinition(actionDefinitionId, parameterDefinitions); + given(actionsMock.getRuleActionDefinitionById(actionDefinitionId)).willReturn(actionDefinition); + + //when + objectUnderTest.validate(action); + + then(actionsMock).should().getRuleActionDefinitionById(actionDefinitionId); + then(actionsMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testValidationPassesWhenNoParametersNeeded() + { + final Action action = new Action(); + final String actionDefinitionId = "properActionDefinition"; + action.setActionDefinitionId(actionDefinitionId); + final ActionDefinition actionDefinition = createActionDefinition(actionDefinitionId, null); + given(actionsMock.getRuleActionDefinitionById(actionDefinitionId)).willReturn(actionDefinition); + + //when + objectUnderTest.validate(action); + + then(actionsMock).should().getRuleActionDefinitionById(actionDefinitionId); + then(actionsMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testValidationPassesWhenNoMandatoryParametersNeeded() + { + final Action action = new Action(); + final String actionDefinitionId = "properActionDefinition"; + action.setActionDefinitionId(actionDefinitionId); + final ActionDefinition actionDefinition = + createActionDefinition(actionDefinitionId, List.of(createParameterDefinition(NON_MANDATORY_PARAM_KEY, TEXT, false, null))); + given(actionsMock.getRuleActionDefinitionById(actionDefinitionId)).willReturn(actionDefinition); + + //when + objectUnderTest.validate(action); + + then(actionsMock).should().getRuleActionDefinitionById(actionDefinitionId); + then(actionsMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testValidationPassesWhenOptionalParametersNotProvided() + { + final Action action = new Action(); + final String actionDefinitionId = "properActionDefinition"; + action.setActionDefinitionId(actionDefinitionId); + action.setParams(Map.of(MANDATORY_PARAM_KEY, "paramValue")); + final List parameterDefinitions = + List.of(createParameterDefinition(MANDATORY_PARAM_KEY, TEXT, true, null), + createParameterDefinition(NON_MANDATORY_PARAM_KEY, BOOLEAN, false, null)); + final ActionDefinition actionDefinition = createActionDefinition(actionDefinitionId, parameterDefinitions); + given(actionsMock.getRuleActionDefinitionById(actionDefinitionId)).willReturn(actionDefinition); + + //when + objectUnderTest.validate(action); + + then(actionsMock).should().getRuleActionDefinitionById(actionDefinitionId); + then(actionsMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testValidationFailsWhenTooManyParameters() + { + final Action action = new Action(); + final String actionDefinitionId = "properActionDefinition"; + action.setActionDefinitionId(actionDefinitionId); + action.setParams(Map.of(MANDATORY_PARAM_KEY, "paramValue", NON_MANDATORY_PARAM_KEY, false)); + final List parameterDefinitions = + List.of(createParameterDefinition(MANDATORY_PARAM_KEY, TEXT, true, null)); + final ActionDefinition actionDefinition = createActionDefinition(actionDefinitionId, parameterDefinitions); + given(actionsMock.getRuleActionDefinitionById(actionDefinitionId)).willReturn(actionDefinition); + + //when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy(() -> objectUnderTest.validate(action)) + .withMessageContaining(String.format(MUST_NOT_CONTAIN_PARAMETER, actionDefinitionId, NON_MANDATORY_PARAM_KEY)); + + then(actionsMock).should().getRuleActionDefinitionById(actionDefinitionId); + then(actionsMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testValidationFailsWhenMissingParameters() + { + final Action action = new Action(); + final String actionDefinitionId = "properActionDefinition"; + action.setActionDefinitionId(actionDefinitionId); + final List parameterDefinitions = + List.of(createParameterDefinition(MANDATORY_PARAM_KEY, TEXT, true, null)); + final ActionDefinition actionDefinition = createActionDefinition(actionDefinitionId, parameterDefinitions); + given(actionsMock.getRuleActionDefinitionById(actionDefinitionId)).willReturn(actionDefinition); + + //when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy(() -> objectUnderTest.validate(action)) + .withMessageContaining(String.format(PARAMS_SHOULD_NOT_BE_EMPTY, actionDefinitionId)); + + then(actionsMock).should().getRuleActionDefinitionById(actionDefinitionId); + then(actionsMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testValidationFailsWhenMissingParameterValue() + { + final Action action = new Action(); + final String actionDefinitionId = "properActionDefinition"; + action.setActionDefinitionId(actionDefinitionId); + final Map params = new HashMap<>(); + params.put(MANDATORY_PARAM_KEY, null); + action.setParams(params); + final List parameterDefinitions = + List.of(createParameterDefinition(MANDATORY_PARAM_KEY, TEXT, true, null)); + final ActionDefinition actionDefinition = createActionDefinition(actionDefinitionId, parameterDefinitions); + given(actionsMock.getRuleActionDefinitionById(actionDefinitionId)).willReturn(actionDefinition); + + //when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy(() -> objectUnderTest.validate(action)) + .withMessageContaining(String.format(MISSING_PARAMETER, MANDATORY_PARAM_KEY)); + + then(actionsMock).should().getRuleActionDefinitionById(actionDefinitionId); + then(actionsMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testValidationFailsWhenMandatoryParameterIsMissing() + { + final Action action = new Action(); + final String actionDefinitionId = "properActionDefinition"; + action.setActionDefinitionId(actionDefinitionId); + action.setParams(Map.of(NON_MANDATORY_PARAM_KEY, true)); + final List parameterDefinitions = + List.of(createParameterDefinition(MANDATORY_PARAM_KEY, TEXT, true, null), + createParameterDefinition(NON_MANDATORY_PARAM_KEY, BOOLEAN, false, null)); + final ActionDefinition actionDefinition = createActionDefinition(actionDefinitionId, parameterDefinitions); + given(actionsMock.getRuleActionDefinitionById(actionDefinitionId)).willReturn(actionDefinition); + + //when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy(() -> objectUnderTest.validate(action)) + .withMessageContaining(String.format(MISSING_PARAMETER, MANDATORY_PARAM_KEY)); + + then(actionsMock).should().getRuleActionDefinitionById(actionDefinitionId); + then(actionsMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testValidationFailsWhenActionWithNullActionDefinition() + { + final Action action = new Action(); + action.setActionDefinitionId(null); + action.setParams(Map.of(MANDATORY_PARAM_KEY, "paramValue")); + + //when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy(() -> objectUnderTest.validate(action)) + .withMessageContaining(EMPTY_ACTION_DEFINITION); + + then(actionsMock).shouldHaveNoInteractions(); + } + + @Test + public void testValidationFailsWhenNotApplicableActionDefinition() + { + final Action action = new Action(); + final String actionDefinitionId = "notApplicableActionDefinition"; + action.setActionDefinitionId(actionDefinitionId); + action.setParams(Map.of(MANDATORY_PARAM_KEY, "paramValue")); + given(actionsMock.getRuleActionDefinitionById(actionDefinitionId)).willThrow(NotFoundException.class); + + //when + assertThatExceptionOfType(InvalidArgumentException.class).isThrownBy(() -> objectUnderTest.validate(action)) + .withMessageContaining(String.format(INVALID_ACTION_DEFINITION, actionDefinitionId)); + + then(actionsMock).should().getRuleActionDefinitionById(actionDefinitionId); + then(actionsMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void testHasProperPriority() + { + final int expectedPriority = Integer.MIN_VALUE; + final int actualPriority = objectUnderTest.getPriority(); + + assertEquals(expectedPriority, actualPriority); + } + + private ActionDefinition createActionDefinition(final String actionDefinitionId, + List parameterDefinitions) + { + return new ActionDefinition(actionDefinitionId, actionDefinitionId, "title", "description", Collections.emptyList(), false, false, + parameterDefinitions); + } + + private ActionDefinition.ParameterDefinition createParameterDefinition(final String name, final QName qName, final boolean mandatory, + final String constraint) + { + return new ActionDefinition.ParameterDefinition(name, qName.toPrefixString(), false, mandatory, "label", constraint); + } + +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/rules/NodeRuleSetsRelationTest.java b/remote-api/src/test/java/org/alfresco/rest/api/rules/NodeRuleSetsRelationTest.java new file mode 100644 index 0000000000..f96d53af37 --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/rules/NodeRuleSetsRelationTest.java @@ -0,0 +1,88 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.rest.api.rules; + +import static org.mockito.BDDMockito.then; + +import junit.framework.TestCase; +import org.alfresco.rest.api.RuleSets; +import org.alfresco.rest.api.model.rules.RuleSetLink; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.List; + +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class NodeRuleSetsRelationTest extends TestCase +{ + private static final String FOLDER_NODE_ID = "dummy-folder-node-id"; + private static final String LINK_TO_NODE_ID = "dummy-link-to-node-id"; + private static final String RULE_SET_NODE_ID = "dummy-rule-set-node-id"; + + @Mock + private RuleSets ruleSets; + + @Mock + private Parameters parameters; + + @InjectMocks + private NodeRuleSetLinksRelation nodeRuleSetLinksRelation; + + @Test + public void shouldProperlyCreateLink() + { + RuleSetLink ruleSetLink = new RuleSetLink(); + List ruleResult = List.of(ruleSetLink); + + RuleSetLink requestBody = new RuleSetLink(); + requestBody.setId(LINK_TO_NODE_ID); + + when(ruleSets.linkToRuleSet(FOLDER_NODE_ID, LINK_TO_NODE_ID)).thenReturn(ruleSetLink); + + List actual = nodeRuleSetLinksRelation.create(FOLDER_NODE_ID,List.of(requestBody), parameters); + Assert.assertEquals(ruleResult, actual); + } + + @Test + public void testUnlinkRuleSet() + { + //when + ruleSets.unlinkRuleSet(FOLDER_NODE_ID,RULE_SET_NODE_ID); + + then(ruleSets).should().unlinkRuleSet(FOLDER_NODE_ID,RULE_SET_NODE_ID); + then(ruleSets).shouldHaveNoMoreInteractions(); + } + + +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/nodes/NodeRulesRelationTest.java b/remote-api/src/test/java/org/alfresco/rest/api/rules/NodeRulesRelationTest.java similarity index 73% rename from remote-api/src/test/java/org/alfresco/rest/api/nodes/NodeRulesRelationTest.java rename to remote-api/src/test/java/org/alfresco/rest/api/rules/NodeRulesRelationTest.java index 6f0dff5332..a887448222 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/nodes/NodeRulesRelationTest.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/rules/NodeRulesRelationTest.java @@ -24,7 +24,11 @@ * #L% */ -package org.alfresco.rest.api.nodes; +package org.alfresco.rest.api.rules; + +import static org.mockito.BDDMockito.then; + +import java.util.List; import junit.framework.TestCase; import org.alfresco.rest.api.Rules; @@ -40,17 +44,15 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnitRunner; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.then; - @Experimental @RunWith(MockitoJUnitRunner.class) public class NodeRulesRelationTest extends TestCase { - private static final String FOLDER_NODE_ID = "dummy-node-id"; private static final String RULE_SET_ID = "dummy-rule-set-id"; private static final String RULE_ID = "dummy-rule-id"; + private static final List INCLUDE = List.of("include-field"); + private static final Paging PAGING = Paging.DEFAULT; @Mock private Rules rulesMock; @@ -68,25 +70,34 @@ public class NodeRulesRelationTest extends TestCase @Test public void testReadAll() { - final Paging paging = Paging.DEFAULT; - final Parameters parameters = ParamsExtender.valueOf(paging, FOLDER_NODE_ID, RULE_SET_ID, null); + final Parameters parameters = ParamsExtender.valueOf(PAGING, FOLDER_NODE_ID, RULE_SET_ID, null, INCLUDE); // when nodeRulesRelation.readAll(FOLDER_NODE_ID, parameters); - then(rulesMock).should().getRules(eq(FOLDER_NODE_ID), eq(RULE_SET_ID), eq(paging)); + then(rulesMock).should().getRules(FOLDER_NODE_ID, RULE_SET_ID, INCLUDE, PAGING); then(rulesMock).shouldHaveNoMoreInteractions(); } @Test public void testReadById() { - final Parameters parameters = ParamsExtender.valueOf(null, FOLDER_NODE_ID, RULE_SET_ID, RULE_ID); + final Parameters parameters = ParamsExtender.valueOf(null, FOLDER_NODE_ID, RULE_SET_ID, RULE_ID, INCLUDE); // when nodeRulesRelation.readById(FOLDER_NODE_ID, RULE_SET_ID, parameters); - then(rulesMock).should().getRuleById(eq(FOLDER_NODE_ID), eq(RULE_SET_ID), eq(RULE_ID)); + then(rulesMock).should().getRuleById(FOLDER_NODE_ID, RULE_SET_ID, RULE_ID, INCLUDE); then(rulesMock).shouldHaveNoMoreInteractions(); } -} \ No newline at end of file + + @Test + public void testDeleteById() { + final Parameters parameters = ParamsExtender.valueOf(null, FOLDER_NODE_ID, RULE_SET_ID, RULE_ID, INCLUDE); + // when + nodeRulesRelation.delete(FOLDER_NODE_ID, RULE_SET_ID, parameters); + + then(rulesMock).should().deleteRuleById(FOLDER_NODE_ID, RULE_SET_ID, RULE_ID); + then(rulesMock).shouldHaveNoMoreInteractions(); + } +} diff --git a/remote-api/src/test/java/org/alfresco/rest/api/tests/DiscoveryApiTest.java b/remote-api/src/test/java/org/alfresco/rest/api/tests/DiscoveryApiTest.java index f244401350..ba70eea362 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/tests/DiscoveryApiTest.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/tests/DiscoveryApiTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2021 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -177,6 +177,7 @@ public class DiscoveryApiTest extends AbstractSingleNetworkSiteTest assertEquals(1000L, entitlements.getMaxDocs().longValue()); assertTrue(entitlements.getIsClusterEnabled()); assertFalse(entitlements.getIsCryptodocEnabled()); + assertFalse(entitlements.getIsCustomEmbeddedWorkflowEnabled()); // Check status StatusInfo statusInfo = repositoryInfo.getStatus(); @@ -297,11 +298,13 @@ public class DiscoveryApiTest extends AbstractSingleNetworkSiteTest assertEquals(1000L, entitlements.getMaxDocs().longValue()); assertTrue(entitlements.getIsClusterEnabled()); assertFalse(entitlements.getIsCryptodocEnabled()); + assertFalse(entitlements.getIsCustomEmbeddedWorkflowEnabled()); // Override entitlements when(licenseDescriptorMock.getMaxDocs()).thenReturn(null); when(licenseDescriptorMock.isClusterEnabled()).thenReturn(false); when(licenseDescriptorMock.isCryptodocEnabled()).thenReturn(true); + when(licenseDescriptorMock.isCustomEmbeddedWorkflowEnabled()).thenReturn(true); response = get("discovery", null, 200); @@ -319,6 +322,7 @@ public class DiscoveryApiTest extends AbstractSingleNetworkSiteTest assertNull(entitlements.getMaxDocs()); assertFalse(entitlements.getIsClusterEnabled()); assertTrue(entitlements.getIsCryptodocEnabled()); + assertTrue(entitlements.getIsCustomEmbeddedWorkflowEnabled()); } @Test diff --git a/remote-api/src/test/java/org/alfresco/rest/api/tests/EnterpriseJettyComponent.java b/remote-api/src/test/java/org/alfresco/rest/api/tests/EnterpriseJettyComponent.java index bb93c253da..0d476b7c10 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/tests/EnterpriseJettyComponent.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/tests/EnterpriseJettyComponent.java @@ -1,34 +1,30 @@ -/* - * #%L - * Alfresco Remote API - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.rest.api.tests; -import org.apache.chemistry.opencmis.server.impl.atompub.CmisAtomPubServlet; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.webapp.WebAppContext; - /** * Manages an embedded jetty server, hooking it up to the repository spring context and providing * authenticated, tenant-based access through the tenant servlet. @@ -42,15 +38,4 @@ public class EnterpriseJettyComponent extends PublicApiJettyComponent { super(port, contextPath, configLocations, classLocations); } - - @Override - protected void configureWebAppContext(WebAppContext webAppContext) - { - super.configureWebAppContext(webAppContext); - - // the tenant servlet with alfresco managed authentication - ServletHolder servletHolder = new ServletHolder(CmisAtomPubServlet.class); - servletHolder.setInitParameter("callContextHandler", "org.apache.chemistry.opencmis.server.shared.BasicAuthCallContextHandler"); - webAppContext.addServlet(servletHolder, "/cmisatom/*"); - } } diff --git a/remote-api/src/test/java/org/alfresco/rest/api/tests/ModulePackagesApiTest.java b/remote-api/src/test/java/org/alfresco/rest/api/tests/ModulePackagesApiTest.java index 6b7b6eb30b..a77542471c 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/tests/ModulePackagesApiTest.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/tests/ModulePackagesApiTest.java @@ -123,7 +123,7 @@ public class ModulePackagesApiTest extends AbstractBaseApiTest assertNotNull(response); assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); assertEquals("no-cache", response.getHeaders().get("Cache-Control")); - assertEquals("application/json;charset=UTF-8", response.getHeaders().get("Content-Type")); + assertEquals("application/json;charset=utf-8", response.getHeaders().get("Content-Type")); PublicApiClient.ExpectedErrorResponse errorResponse = RestApiUtil.parseErrorResponse(response.getJsonResponse()); assertNotNull(errorResponse); diff --git a/remote-api/src/test/java/org/alfresco/rest/api/tests/NodeApiTest.java b/remote-api/src/test/java/org/alfresco/rest/api/tests/NodeApiTest.java index bba1bd19f7..1b98fe3018 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/tests/NodeApiTest.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/tests/NodeApiTest.java @@ -106,6 +106,7 @@ import org.apache.commons.collections.map.MultiValueMap; import org.json.simple.JSONObject; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; /** @@ -3611,6 +3612,7 @@ public class NodeApiTest extends AbstractSingleNetworkSiteTest * {@literal :/alfresco/api/-default-/public/alfresco/versions/1/nodes//content} */ @Test + @Ignore("ACS-3531 Frequent intermittent failure") public void testDownloadFileContent() throws Exception { setRequestContext(user1); @@ -3721,6 +3723,50 @@ public class NodeApiTest extends AbstractSingleNetworkSiteTest getSingle(getNodeContentUrl(contentNodeId), null, null, headers, 304); } + /** + * Tests download of file/content name. + *

GET:

+ * {@literal :/alfresco/api/-default-/public/alfresco/versions/1/nodes//content} + */ + @Test + public void testDownloadFileContentName() throws Exception + { + setRequestContext(user1); + + // + // Test plain text + // + + String fileName = "Test Download (1).txt"; + File file = getResourceFile(fileName); + + MultiPartBuilder multiPartBuilder = MultiPartBuilder.create() + .setFileData(new FileData(fileName, file)); + MultiPartRequest reqBody = multiPartBuilder.build(); + + // Upload text content + HttpResponse response = post(getNodeChildrenUrl(Nodes.PATH_MY), reqBody.getBody(), null, reqBody.getContentType(), 201); + Document document = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Document.class); + + String contentNodeId = document.getId(); + + // Check the upload response + assertEquals(fileName, document.getName()); + ContentInfo contentInfo = document.getContent(); + assertNotNull(contentInfo); + assertEquals(MimetypeMap.MIMETYPE_TEXT_PLAIN, contentInfo.getMimeType()); + + // Download text content - by default with Content-Disposition header + response = getSingle(NodesEntityResource.class, contentNodeId + "/content", null, 200); + + String textContent = response.getResponse(); + assertEquals("The quick brown fox jumps over the lazy dog", textContent); + Map responseHeaders = response.getHeaders(); + assertNotNull(responseHeaders); + assertEquals("attachment; filename=\"Test Download (1).txt\"; filename*=UTF-8''Test%20Download%20%281%29.txt", responseHeaders.get("Content-Disposition")); + + } + /** * Tests download of file/content using backed temp file for streaming. *

diff --git a/remote-api/src/test/java/org/alfresco/rest/api/tests/PublicApiJettyComponent.java b/remote-api/src/test/java/org/alfresco/rest/api/tests/PublicApiJettyComponent.java index 8924fc1678..7728491a79 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/tests/PublicApiJettyComponent.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/tests/PublicApiJettyComponent.java @@ -1,28 +1,28 @@ -/* - * #%L - * Alfresco Remote API - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.rest.api.tests; import org.alfresco.repo.web.util.AbstractJettyComponent; @@ -48,63 +48,16 @@ public class PublicApiJettyComponent extends AbstractJettyComponent @Override protected void configureWebAppContext(WebAppContext webAppContext) { -// ServletContext servletContext = webAppContext.getServletContext(); // the tenant servlet with alfresco managed authentication ServletHolder servletHolder = new ServletHolder(PublicApiWebScriptServlet.class); servletHolder.setInitParameter("authenticator", "publicapi.authenticator"); webAppContext.addServlet(servletHolder, "/" + publicApiServletName + "/*"); - -// DependencyInjectedFilter apiFilter = (DependencyInjectedFilter)getApplicationContext().getBean("publicAPICMISFilter"); -// BeanProxyFilter filter = new BeanProxyFilter(servletContext, apiFilter); -// FilterHolder filterHolder = new FilterHolder(filter); -// webAppContext.addFilter(filterHolder, "/" + publicApiServletName + "/*", null); + // the tenant servlet with alfresco managed authentication servletHolder = new ServletHolder(CmisAtomPubServlet.class); servletHolder.setInitParameter("callContextHandler", "org.apache.chemistry.opencmis.server.shared.BasicAuthCallContextHandler"); - webAppContext.addServlet(servletHolder, "/cmisatom/*"); + webAppContext.addServlet(servletHolder, "/cmisatom/*"); } - -// private static class BeanProxyFilter implements Filter -// { -// private DependencyInjectedFilter filter; -// private ServletContext context; -// -// private BeanProxyFilter(ServletContext context, DependencyInjectedFilter filter) -// { -// this.context = context; -// this.filter = filter; -// } -// -// /** -// * Initialize the filter. -// * -// * @param args -// * FilterConfig -// * @throws ServletException -// * the servlet exception -// * @exception ServletException -// */ -// public void init(FilterConfig args) throws ServletException -// { -// } -// -// /* (non-Javadoc) -// * @see javax.servlet.Filter#destroy() -// */ -// public void destroy() -// { -// this.filter = null; -// } -// -// /* (non-Javadoc) -// * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) -// */ -// public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, -// ServletException -// { -// this.filter.doFilter(this.context, request, response, chain); -// } -// } } diff --git a/remote-api/src/test/java/org/alfresco/rest/api/tests/SharedLinkApiTest.java b/remote-api/src/test/java/org/alfresco/rest/api/tests/SharedLinkApiTest.java index fa64a0af42..3faadffb97 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/tests/SharedLinkApiTest.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/tests/SharedLinkApiTest.java @@ -303,7 +303,7 @@ public class SharedLinkApiTest extends AbstractBaseApiTest assertArrayEquals(file1_originalBytes, response.getResponseAsBytes()); Map responseHeaders = response.getHeaders(); assertNotNull(responseHeaders); - assertEquals(file1_MimeType+";charset=UTF-8", responseHeaders.get("Content-Type")); + assertEquals(file1_MimeType+";charset=utf-8", responseHeaders.get("Content-Type")); assertNotNull(responseHeaders.get("Expires")); assertEquals("attachment; filename=\"" + fileName1 + "\"; filename*=UTF-8''" + fileName1 + "", responseHeaders.get("Content-Disposition")); String lastModifiedHeader = responseHeaders.get(LAST_MODIFIED_HEADER); @@ -319,7 +319,7 @@ public class SharedLinkApiTest extends AbstractBaseApiTest assertArrayEquals(file1_originalBytes, response.getResponseAsBytes()); responseHeaders = response.getHeaders(); assertNotNull(responseHeaders); - assertEquals(file1_MimeType+";charset=UTF-8", responseHeaders.get("Content-Type")); + assertEquals(file1_MimeType+";charset=utf-8", responseHeaders.get("Content-Type")); assertNotNull(responseHeaders.get(LAST_MODIFIED_HEADER)); assertNotNull(responseHeaders.get("Expires")); assertNull(responseHeaders.get("Content-Disposition")); @@ -330,7 +330,7 @@ public class SharedLinkApiTest extends AbstractBaseApiTest assertArrayEquals(content2Text.getBytes(), response.getResponseAsBytes()); responseHeaders = response.getHeaders(); assertNotNull(responseHeaders); - assertEquals(file2_MimeType+";charset=ISO-8859-1", responseHeaders.get("Content-Type")); + assertEquals(file2_MimeType+";charset=iso-8859-1", responseHeaders.get("Content-Type")); assertNotNull(responseHeaders.get("Expires")); assertNotNull(responseHeaders.get(LAST_MODIFIED_HEADER)); assertEquals("attachment; filename=\"" + fileName2 + "\"; filename*=UTF-8''" + fileName2 + "", responseHeaders.get("Content-Disposition")); @@ -392,7 +392,7 @@ public class SharedLinkApiTest extends AbstractBaseApiTest assertTrue(response.getResponseAsBytes().length > 0); responseHeaders = response.getHeaders(); assertNotNull(responseHeaders); - assertEquals(MimetypeMap.MIMETYPE_IMAGE_PNG+";charset=UTF-8", responseHeaders.get("Content-Type")); + assertEquals(MimetypeMap.MIMETYPE_IMAGE_PNG+";charset=utf-8", responseHeaders.get("Content-Type")); assertNotNull(responseHeaders.get(LAST_MODIFIED_HEADER)); assertNotNull(responseHeaders.get("Expires")); String docName = "doclib"; @@ -405,7 +405,7 @@ public class SharedLinkApiTest extends AbstractBaseApiTest assertTrue(response.getResponseAsBytes().length > 0); responseHeaders = response.getHeaders(); assertNotNull(responseHeaders); - assertEquals(MimetypeMap.MIMETYPE_IMAGE_PNG+";charset=UTF-8", responseHeaders.get("Content-Type")); + assertEquals(MimetypeMap.MIMETYPE_IMAGE_PNG+";charset=utf-8", responseHeaders.get("Content-Type")); assertNotNull(responseHeaders.get("Expires")); assertNull(responseHeaders.get("Content-Disposition")); lastModifiedHeader = responseHeaders.get(LAST_MODIFIED_HEADER); @@ -816,7 +816,7 @@ public class SharedLinkApiTest extends AbstractBaseApiTest assertArrayEquals(file1_originalBytes, response.getResponseAsBytes()); Map responseHeaders = response.getHeaders(); assertNotNull(responseHeaders); - assertEquals(file1_MimeType + ";charset=UTF-8", responseHeaders.get("Content-Type")); + assertEquals(file1_MimeType + ";charset=utf-8", responseHeaders.get("Content-Type")); assertNotNull(responseHeaders.get("Expires")); assertEquals("attachment; filename=\"" + fileName1 + "\"; filename*=UTF-8''" + fileName1 + "", responseHeaders.get("Content-Disposition")); String lastModifiedHeader = responseHeaders.get(LAST_MODIFIED_HEADER); @@ -832,7 +832,7 @@ public class SharedLinkApiTest extends AbstractBaseApiTest assertArrayEquals(file1_originalBytes, response.getResponseAsBytes()); responseHeaders = response.getHeaders(); assertNotNull(responseHeaders); - assertEquals(file1_MimeType + ";charset=UTF-8", responseHeaders.get("Content-Type")); + assertEquals(file1_MimeType + ";charset=utf-8", responseHeaders.get("Content-Type")); assertNotNull(responseHeaders.get(LAST_MODIFIED_HEADER)); assertNotNull(responseHeaders.get("Expires")); assertNull(responseHeaders.get("Content-Disposition")); @@ -888,7 +888,7 @@ public class SharedLinkApiTest extends AbstractBaseApiTest assertTrue(response.getResponseAsBytes().length > 0); responseHeaders = response.getHeaders(); assertNotNull(responseHeaders); - assertEquals(MimetypeMap.MIMETYPE_IMAGE_PNG + ";charset=UTF-8", responseHeaders.get("Content-Type")); + assertEquals(MimetypeMap.MIMETYPE_IMAGE_PNG + ";charset=utf-8", responseHeaders.get("Content-Type")); assertNotNull(responseHeaders.get(LAST_MODIFIED_HEADER)); assertNotNull(responseHeaders.get("Expires")); String docName = "doclib"; @@ -901,7 +901,7 @@ public class SharedLinkApiTest extends AbstractBaseApiTest assertTrue(response.getResponseAsBytes().length > 0); responseHeaders = response.getHeaders(); assertNotNull(responseHeaders); - assertEquals(MimetypeMap.MIMETYPE_IMAGE_PNG + ";charset=UTF-8", responseHeaders.get("Content-Type")); + assertEquals(MimetypeMap.MIMETYPE_IMAGE_PNG + ";charset=utf-8", responseHeaders.get("Content-Type")); assertNotNull(responseHeaders.get("Expires")); assertNull(responseHeaders.get("Content-Disposition")); lastModifiedHeader = responseHeaders.get(LAST_MODIFIED_HEADER); diff --git a/remote-api/src/test/java/org/alfresco/rest/framework/tests/core/ParamsExtender.java b/remote-api/src/test/java/org/alfresco/rest/framework/tests/core/ParamsExtender.java index ce26c504d4..af29e9104f 100755 --- a/remote-api/src/test/java/org/alfresco/rest/framework/tests/core/ParamsExtender.java +++ b/remote-api/src/test/java/org/alfresco/rest/framework/tests/core/ParamsExtender.java @@ -28,6 +28,7 @@ package org.alfresco.rest.framework.tests.core; import static org.mockito.Mockito.mock; import java.io.InputStream; +import java.util.List; import java.util.Map; import org.alfresco.rest.framework.jacksonextensions.BeanPropertiesFilter; @@ -72,4 +73,9 @@ public class ParamsExtender extends Params { return new ParamsExtender(entityId, relationshipId, relationship2Id, null, null, null, new Params.RecognizedParams(null, paging, null, null, null, null, null, null, false)); } + + public static Params valueOf(Paging paging, String entityId, String relationshipId, String relationship2Id, List include) + { + return new ParamsExtender(entityId, relationshipId, relationship2Id, null, null, null, new Params.RecognizedParams(null, paging, null, null, include, null, null, null, false)); + } } diff --git a/remote-api/src/test/java/org/alfresco/rest/workflow/api/tests/DeploymentWorkflowApiTest.java b/remote-api/src/test/java/org/alfresco/rest/workflow/api/tests/DeploymentWorkflowApiTest.java index b74af43523..48b767529e 100644 --- a/remote-api/src/test/java/org/alfresco/rest/workflow/api/tests/DeploymentWorkflowApiTest.java +++ b/remote-api/src/test/java/org/alfresco/rest/workflow/api/tests/DeploymentWorkflowApiTest.java @@ -38,6 +38,7 @@ import java.util.Map; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.tenant.TenantUtil; import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.workflow.WorkflowDeployer; import org.alfresco.rest.api.tests.AbstractTestFixture; import org.alfresco.rest.api.tests.RepoService.TestNetwork; import org.alfresco.rest.api.tests.client.PublicApiClient.ListResponse; @@ -128,7 +129,7 @@ public class DeploymentWorkflowApiTest extends EnterpriseWorkflowTestApi Deployment adhocDeployment = deploymentMap.get("adhoc.bpmn20.xml"); assertEquals(activitiDeployment.getId(), adhocDeployment.getId()); - assertEquals(activitiDeployment.getCategory(), adhocDeployment.getCategory()); + assertEquals(activitiDeployment.getCategory(), WorkflowDeployer.CATEGORY_FULL_ACCESS); assertEquals(activitiDeployment.getName(), adhocDeployment.getName()); assertEquals(activitiDeployment.getDeploymentTime(), adhocDeployment.getDeployedAt()); @@ -251,7 +252,7 @@ public class DeploymentWorkflowApiTest extends EnterpriseWorkflowTestApi assertNotNull(deployment); assertEquals(activitiDeployment.getId(), deployment.getId()); - assertEquals(activitiDeployment.getCategory(), deployment.getCategory()); + assertEquals(activitiDeployment.getCategory(), WorkflowDeployer.CATEGORY_FULL_ACCESS); assertEquals(activitiDeployment.getName(), deployment.getName()); assertEquals(activitiDeployment.getDeploymentTime(), deployment.getDeployedAt()); diff --git a/remote-api/src/test/resources/publicapi/upload/Test Download (1).txt b/remote-api/src/test/resources/publicapi/upload/Test Download (1).txt new file mode 100644 index 0000000000..ff3bb63948 --- /dev/null +++ b/remote-api/src/test/resources/publicapi/upload/Test Download (1).txt @@ -0,0 +1 @@ +The quick brown fox jumps over the lazy dog \ No newline at end of file diff --git a/repository/pom.xml b/repository/pom.xml index b716524aa0..95d21a27bb 100644 --- a/repository/pom.xml +++ b/repository/pom.xml @@ -7,7 +7,7 @@ org.alfresco alfresco-community-repo - 17.43-SNAPSHOT + 20.5-SNAPSHOT @@ -236,7 +236,7 @@ org.freemarker freemarker - 2.3.20-alfresco-patched-20220413 + 2.3.31-alfresco-patched org.apache.xmlbeans @@ -300,8 +300,8 @@ org.apache.poi - ooxml-schemas - ${dependency.ooxml-schemas.version} + poi-ooxml-lite + ${dependency.poi-ooxml-lite.version} org.apache.poi @@ -374,7 +374,7 @@ com.fasterxml.woodstox woodstox-core - 6.3.0 + 6.3.1 @@ -691,6 +691,16 @@ camel-mock test + + + io.netty + netty-handler-proxy + + + io.netty + netty-tcnative-classes + + org.apache.activemq activemq-client @@ -732,6 +742,11 @@ junit test + + org.assertj + assertj-core + test + org.alfresco alfresco-transform-model diff --git a/repository/src/main/java/org/alfresco/repo/action/ParameterizedItemImpl.java b/repository/src/main/java/org/alfresco/repo/action/ParameterizedItemImpl.java index f5a8d165ee..1783785308 100644 --- a/repository/src/main/java/org/alfresco/repo/action/ParameterizedItemImpl.java +++ b/repository/src/main/java/org/alfresco/repo/action/ParameterizedItemImpl.java @@ -1,33 +1,34 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.repo.action; import java.io.Serializable; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import org.alfresco.service.cmr.action.ParameterizedItem; @@ -38,8 +39,8 @@ import org.alfresco.service.cmr.action.ParameterizedItem; */ public abstract class ParameterizedItemImpl implements ParameterizedItem, Serializable { - private static final long serialVersionUID = 3578052215076397741L; - + private static final long serialVersionUID = 3578052215076397741L; + /** * The id */ @@ -50,23 +51,23 @@ public abstract class ParameterizedItemImpl implements ParameterizedItem, Serial */ private Map parameterValues = new HashMap(); - /** - * Constructor - * - * @param id the id - */ - public ParameterizedItemImpl(String id) - { + /** + * Constructor + * + * @param id the id + */ + public ParameterizedItemImpl(String id) + { this(id, null); } - /** - * Constructor - * - * @param id the rule item - * @param parameterValues the parameter values - */ - public ParameterizedItemImpl(String id, Map parameterValues) + /** + * Constructor + * + * @param id the rule item + * @param parameterValues the parameter values + */ + public ParameterizedItemImpl(String id, Map parameterValues) { // Set the action id this.id = id; @@ -135,7 +136,7 @@ public abstract class ParameterizedItemImpl implements ParameterizedItem, Serial @Override public int hashCode() { - return this.id.hashCode(); + return Objects.hashCode(this.id); } /** diff --git a/repository/src/main/java/org/alfresco/repo/action/access/ActionAccessRestrictionAbstractBase.java b/repository/src/main/java/org/alfresco/repo/action/access/ActionAccessRestrictionAbstractBase.java index 0139d7515b..af847ec7f4 100644 --- a/repository/src/main/java/org/alfresco/repo/action/access/ActionAccessRestrictionAbstractBase.java +++ b/repository/src/main/java/org/alfresco/repo/action/access/ActionAccessRestrictionAbstractBase.java @@ -101,8 +101,8 @@ public abstract class ActionAccessRestrictionAbstractBase implements ActionAcces if (context != null) { return Stream.of( - getConfigKey(context, actionName), - getConfigKey(context)); + getConfigKey(actionName, context), + getConfigKey(actionName)); } return Stream.of(getConfigKey(actionName)); } diff --git a/repository/src/main/java/org/alfresco/repo/action/constraint/AspectParameterConstraint.java b/repository/src/main/java/org/alfresco/repo/action/constraint/AspectParameterConstraint.java index 2d020c4c80..4e83a711ca 100644 --- a/repository/src/main/java/org/alfresco/repo/action/constraint/AspectParameterConstraint.java +++ b/repository/src/main/java/org/alfresco/repo/action/constraint/AspectParameterConstraint.java @@ -1,34 +1,34 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.repo.action.constraint; -import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import org.alfresco.service.cmr.dictionary.AspectDefinition; import org.alfresco.service.cmr.dictionary.DictionaryService; @@ -55,19 +55,22 @@ public class AspectParameterConstraint extends BaseParameterConstraint * @see org.alfresco.service.cmr.action.ParameterConstraint#getAllowableValues() */ protected Map getAllowableValuesImpl() - { - Collection aspects = dictionaryService.getAllAspects(); - Map result = new LinkedHashMap(aspects.size()); - for (QName aspect : aspects) - { - AspectDefinition aspectDef = dictionaryService.getAspect(aspect); - if (aspectDef != null && aspectDef.getTitle(dictionaryService) != null) - { - result.put(aspect.toPrefixString(), aspectDef.getTitle(dictionaryService)); - } - } - return result; - } - - + { + final Map values = getValues(); + values.values().removeIf(Objects::isNull); + return values; + } + + @Override + public Map getValues() + { + return dictionaryService.getAllAspects().stream() + .collect(LinkedHashMap::new, (m, v) -> m.put(v.toPrefixString(), getTitle(v)), LinkedHashMap::putAll); + } + + private String getTitle(QName aspect) + { + final AspectDefinition aspectDef = dictionaryService.getAspect(aspect); + return aspectDef != null ? aspectDef.getTitle(dictionaryService) : null; + } } diff --git a/repository/src/main/java/org/alfresco/repo/action/constraint/PropertyParameterConstraint.java b/repository/src/main/java/org/alfresco/repo/action/constraint/PropertyParameterConstraint.java index c9da558482..03e747d9ba 100644 --- a/repository/src/main/java/org/alfresco/repo/action/constraint/PropertyParameterConstraint.java +++ b/repository/src/main/java/org/alfresco/repo/action/constraint/PropertyParameterConstraint.java @@ -1,34 +1,34 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.repo.action.constraint; -import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.PropertyDefinition; @@ -55,17 +55,22 @@ public class PropertyParameterConstraint extends BaseParameterConstraint * @see org.alfresco.service.cmr.action.ParameterConstraint#getAllowableValues() */ protected Map getAllowableValuesImpl() - { - Collection properties = dictionaryService.getAllProperties(null); - Map result = new LinkedHashMap(properties.size()); - for (QName property : properties) - { - PropertyDefinition propertyDef = dictionaryService.getProperty(property); - if (propertyDef != null && propertyDef.getTitle(dictionaryService) != null) - { - result.put(property.toPrefixString(), propertyDef.getTitle(dictionaryService)); - } - } - return result; - } + { + final Map values = getValues(); + values.values().removeIf(Objects::isNull); + return values; + } + + @Override + public Map getValues() + { + return dictionaryService.getAllProperties(null).stream() + .collect(LinkedHashMap::new, (m, v) -> m.put(v.toPrefixString(), getTitle(v)), LinkedHashMap::putAll); + } + + private String getTitle(QName property) + { + final PropertyDefinition propertyDef = dictionaryService.getProperty(property); + return propertyDef != null ? propertyDef.getTitle(dictionaryService) : null; + } } diff --git a/repository/src/main/java/org/alfresco/repo/action/constraint/TypeParameterConstraint.java b/repository/src/main/java/org/alfresco/repo/action/constraint/TypeParameterConstraint.java index 639c98e14c..72e49b5878 100644 --- a/repository/src/main/java/org/alfresco/repo/action/constraint/TypeParameterConstraint.java +++ b/repository/src/main/java/org/alfresco/repo/action/constraint/TypeParameterConstraint.java @@ -1,34 +1,35 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.repo.action.constraint; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.TypeDefinition; @@ -55,17 +56,22 @@ public class TypeParameterConstraint extends BaseParameterConstraint * @see org.alfresco.service.cmr.action.ParameterConstraint#getAllowableValues() */ protected Map getAllowableValuesImpl() - { - Collection types = dictionaryService.getAllTypes(); - Map result = new LinkedHashMap(types.size()); - for (QName type : types) - { - TypeDefinition typeDef = dictionaryService.getType(type); - if (typeDef != null && typeDef.getTitle(dictionaryService) != null) - { - result.put(type.toPrefixString(), typeDef.getTitle(dictionaryService)); - } - } - return result; - } + { + final Map values = getValues(); + values.values().removeIf(Objects::isNull); + return values; + } + + @Override + public Map getValues() + { + return dictionaryService.getAllTypes().stream() + .collect(LinkedHashMap::new, (m, v) -> m.put(v.toPrefixString(), getTitle(v)), LinkedHashMap::putAll); + } + + private String getTitle(QName type) + { + final TypeDefinition typeDef = dictionaryService.getType(type); + return typeDef != null ? typeDef.getTitle(dictionaryService) : null; + } } diff --git a/repository/src/main/java/org/alfresco/repo/attributes/PropTablesCleaner.java b/repository/src/main/java/org/alfresco/repo/attributes/PropTablesCleaner.java index 5d859fcca1..03e7662422 100644 --- a/repository/src/main/java/org/alfresco/repo/attributes/PropTablesCleaner.java +++ b/repository/src/main/java/org/alfresco/repo/attributes/PropTablesCleaner.java @@ -46,6 +46,7 @@ public class PropTablesCleaner { private static final String PROPERTY_PROP_TABLE_CLEANER_ALG = "system.prop_table_cleaner.algorithm"; private static final String PROP_TABLE_CLEANER_ALG_V2 = "V2"; + private static final String PROP_TABLE_CLEANER_ALG_V3 = "V3"; private PropertyValueDAO propertyValueDAO; private JobLockService jobLockService; @@ -100,6 +101,10 @@ public class PropTablesCleaner { propertyValueDAO.cleanupUnusedValuesV2(); } + else if (PROP_TABLE_CLEANER_ALG_V3.equalsIgnoreCase(getAlgorithm())) + { + propertyValueDAO.cleanupUnusedValuesV3(); + } else { propertyValueDAO.cleanupUnusedValues(); diff --git a/repository/src/main/java/org/alfresco/repo/content/metadata/AsynchronousExtractor.java b/repository/src/main/java/org/alfresco/repo/content/metadata/AsynchronousExtractor.java index f186060ca1..ab45a16c76 100644 --- a/repository/src/main/java/org/alfresco/repo/content/metadata/AsynchronousExtractor.java +++ b/repository/src/main/java/org/alfresco/repo/content/metadata/AsynchronousExtractor.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2021 Alfresco Software Limited + * Copyright (C) 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -50,7 +50,7 @@ import org.alfresco.service.namespace.NamespaceException; import org.alfresco.service.namespace.NamespacePrefixResolver; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; -import org.alfresco.transform.client.registry.TransformServiceRegistry; +import org.alfresco.transform.registry.TransformServiceRegistry; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVRecord; diff --git a/repository/src/main/java/org/alfresco/repo/content/transform/AbstractLocalTransform.java b/repository/src/main/java/org/alfresco/repo/content/transform/AbstractLocalTransform.java index bb50de6f8d..355df7cb8d 100644 --- a/repository/src/main/java/org/alfresco/repo/content/transform/AbstractLocalTransform.java +++ b/repository/src/main/java/org/alfresco/repo/content/transform/AbstractLocalTransform.java @@ -31,9 +31,9 @@ import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.MimetypeService; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.transform.client.model.config.TransformOption; -import org.alfresco.transform.client.model.config.TransformOptionGroup; -import org.alfresco.transform.client.model.config.TransformOptionValue; +import org.alfresco.transform.config.TransformOption; +import org.alfresco.transform.config.TransformOptionGroup; +import org.alfresco.transform.config.TransformOptionValue; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; diff --git a/repository/src/main/java/org/alfresco/repo/content/transform/AdminUiTransformerDebug.java b/repository/src/main/java/org/alfresco/repo/content/transform/AdminUiTransformerDebug.java index 655047b001..90bb424d1b 100644 --- a/repository/src/main/java/org/alfresco/repo/content/transform/AdminUiTransformerDebug.java +++ b/repository/src/main/java/org/alfresco/repo/content/transform/AdminUiTransformerDebug.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2019 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -37,8 +37,8 @@ import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; -import org.alfresco.transform.client.registry.SupportedTransform; -import org.alfresco.transform.client.registry.TransformServiceRegistry; +import org.alfresco.transform.registry.SupportedTransform; +import org.alfresco.transform.registry.TransformServiceRegistry; import org.alfresco.util.PropertyCheck; import org.alfresco.util.TempFileProvider; import org.springframework.beans.BeansException; diff --git a/repository/src/main/java/org/alfresco/repo/content/transform/DummyTransformServiceRegistry.java b/repository/src/main/java/org/alfresco/repo/content/transform/DummyTransformServiceRegistry.java index a664d3cde6..03f8546960 100644 --- a/repository/src/main/java/org/alfresco/repo/content/transform/DummyTransformServiceRegistry.java +++ b/repository/src/main/java/org/alfresco/repo/content/transform/DummyTransformServiceRegistry.java @@ -25,8 +25,8 @@ */ package org.alfresco.repo.content.transform; -import org.alfresco.transform.client.model.config.CoreFunction; -import org.alfresco.transform.client.registry.TransformServiceRegistry; +import org.alfresco.transform.config.CoreFunction; +import org.alfresco.transform.registry.TransformServiceRegistry; import java.util.Map; diff --git a/repository/src/main/java/org/alfresco/repo/content/transform/LocalFailoverTransform.java b/repository/src/main/java/org/alfresco/repo/content/transform/LocalFailoverTransform.java index a78648d3ac..e1b59f3505 100644 --- a/repository/src/main/java/org/alfresco/repo/content/transform/LocalFailoverTransform.java +++ b/repository/src/main/java/org/alfresco/repo/content/transform/LocalFailoverTransform.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2019 Alfresco Software Limited + * Copyright (C) 2019-2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -31,7 +31,7 @@ import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.MimetypeService; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.transform.client.model.config.TransformOption; +import org.alfresco.transform.config.TransformOption; import org.alfresco.util.TempFileProvider; import java.io.File; diff --git a/repository/src/main/java/org/alfresco/repo/content/transform/LocalPassThroughTransform.java b/repository/src/main/java/org/alfresco/repo/content/transform/LocalPassThroughTransform.java index f1b59f1500..61f8d615c1 100644 --- a/repository/src/main/java/org/alfresco/repo/content/transform/LocalPassThroughTransform.java +++ b/repository/src/main/java/org/alfresco/repo/content/transform/LocalPassThroughTransform.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2021 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -31,11 +31,11 @@ import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.MimetypeService; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.transform.client.model.config.SupportedSourceAndTarget; -import org.alfresco.transform.client.model.config.TransformOption; -import org.alfresco.transform.client.model.config.Transformer; -import org.alfresco.transform.client.registry.AbstractTransformRegistry; -import org.alfresco.transform.client.registry.CombinedConfig; +import org.alfresco.transform.config.SupportedSourceAndTarget; +import org.alfresco.transform.config.TransformOption; +import org.alfresco.transform.config.Transformer; +import org.alfresco.transform.registry.AbstractTransformRegistry; +import org.alfresco.transform.registry.CombinedConfig; import java.io.IOException; import java.io.InputStreamReader; diff --git a/repository/src/main/java/org/alfresco/repo/content/transform/LocalPipelineTransform.java b/repository/src/main/java/org/alfresco/repo/content/transform/LocalPipelineTransform.java index f459bb2b9e..4cbd49bba3 100644 --- a/repository/src/main/java/org/alfresco/repo/content/transform/LocalPipelineTransform.java +++ b/repository/src/main/java/org/alfresco/repo/content/transform/LocalPipelineTransform.java @@ -30,7 +30,7 @@ import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.MimetypeService; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.transform.client.model.config.TransformOption; +import org.alfresco.transform.config.TransformOption; import org.alfresco.util.TempFileProvider; import java.io.File; @@ -39,7 +39,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -import static org.alfresco.transform.client.util.RequestParamMap.DIRECT_ACCESS_URL; +import static org.alfresco.transform.common.RequestParamMap.DIRECT_ACCESS_URL; /** * Transformer that passes a document through a pipeline of transformations to arrive at an target mimetype. diff --git a/repository/src/main/java/org/alfresco/repo/content/transform/LocalTransformImpl.java b/repository/src/main/java/org/alfresco/repo/content/transform/LocalTransformImpl.java index fade0e7316..942afac149 100644 --- a/repository/src/main/java/org/alfresco/repo/content/transform/LocalTransformImpl.java +++ b/repository/src/main/java/org/alfresco/repo/content/transform/LocalTransformImpl.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2021 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -31,7 +31,7 @@ import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.MimetypeService; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.transform.client.model.config.TransformOption; +import org.alfresco.transform.config.TransformOption; import org.alfresco.util.Pair; import java.util.HashMap; diff --git a/repository/src/main/java/org/alfresco/repo/content/transform/LocalTransformServiceRegistry.java b/repository/src/main/java/org/alfresco/repo/content/transform/LocalTransformServiceRegistry.java index 117def527c..74447ad753 100644 --- a/repository/src/main/java/org/alfresco/repo/content/transform/LocalTransformServiceRegistry.java +++ b/repository/src/main/java/org/alfresco/repo/content/transform/LocalTransformServiceRegistry.java @@ -36,14 +36,14 @@ import java.util.function.Consumer; import java.util.stream.Collectors; import org.alfresco.service.cmr.repository.MimetypeService; -import org.alfresco.transform.client.model.config.CoreFunction; -import org.alfresco.transform.client.model.config.TransformOptionGroup; -import org.alfresco.transform.client.registry.CombinedConfig; -import org.alfresco.transform.client.model.config.TransformOption; -import org.alfresco.transform.client.registry.TransformServiceRegistryImpl; -import org.alfresco.transform.client.model.config.TransformStep; -import org.alfresco.transform.client.model.config.Transformer; -import org.alfresco.transform.client.registry.TransformServiceRegistry; +import org.alfresco.transform.config.CoreFunction; +import org.alfresco.transform.config.TransformOptionGroup; +import org.alfresco.transform.registry.CombinedConfig; +import org.alfresco.transform.config.TransformOption; +import org.alfresco.transform.registry.TransformServiceRegistryImpl; +import org.alfresco.transform.config.TransformStep; +import org.alfresco.transform.config.Transformer; +import org.alfresco.transform.registry.TransformServiceRegistry; import org.alfresco.util.PropertyCheck; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; diff --git a/repository/src/main/java/org/alfresco/repo/descriptor/DescriptorStartupLog.java b/repository/src/main/java/org/alfresco/repo/descriptor/DescriptorStartupLog.java index 181334939b..0cfd681737 100644 --- a/repository/src/main/java/org/alfresco/repo/descriptor/DescriptorStartupLog.java +++ b/repository/src/main/java/org/alfresco/repo/descriptor/DescriptorStartupLog.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2018 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -25,7 +25,6 @@ */ package org.alfresco.repo.descriptor; -import java.security.Principal; import java.util.Date; import java.util.Properties; @@ -125,6 +124,15 @@ public class DescriptorStartupLog extends AbstractLifecycleBean { msg += ", NO CLUSTER"; } + + if(license.isCustomEmbeddedWorkflowEnabled()) + { + msg += ", customEmbeddedWorkflow:enabled"; + } + else + { + msg += ", NO CUSTOM EMBEDDED WORKFLOW"; + } String holder = license.getHolderOrganisation(); if (holder != null) diff --git a/repository/src/main/java/org/alfresco/repo/domain/propval/PropertyValueDAO.java b/repository/src/main/java/org/alfresco/repo/domain/propval/PropertyValueDAO.java index fab616299b..a57fc0ab12 100644 --- a/repository/src/main/java/org/alfresco/repo/domain/propval/PropertyValueDAO.java +++ b/repository/src/main/java/org/alfresco/repo/domain/propval/PropertyValueDAO.java @@ -377,4 +377,6 @@ public interface PropertyValueDAO void cleanupUnusedValues(); void cleanupUnusedValuesV2(); + + void cleanupUnusedValuesV3(); } diff --git a/repository/src/main/java/org/alfresco/repo/domain/propval/ibatis/PropertyValueDAOImpl.java b/repository/src/main/java/org/alfresco/repo/domain/propval/ibatis/PropertyValueDAOImpl.java index 7df053318a..d0142a2758 100644 --- a/repository/src/main/java/org/alfresco/repo/domain/propval/ibatis/PropertyValueDAOImpl.java +++ b/repository/src/main/java/org/alfresco/repo/domain/propval/ibatis/PropertyValueDAOImpl.java @@ -758,4 +758,23 @@ public class PropertyValueDAOImpl extends AbstractPropertyValueDAOImpl clearCaches(); } } + + @Override + public void cleanupUnusedValuesV3() + { + // Run the main script + try + { + scriptExecutor.exec(false, "alfresco/dbscripts/utility/${db.script.dialect}", "CleanAlfPropTablesV3.sql"); + } + catch (RuntimeException e) + { + logger.error("The cleanup script failed: ", e); + throw e; + } + finally + { + clearCaches(); + } + } } diff --git a/repository/src/main/java/org/alfresco/repo/domain/schema/script/DeleteNotExistsExecutor.java b/repository/src/main/java/org/alfresco/repo/domain/schema/script/DeleteNotExistsExecutor.java index 26ee58e370..8c7ee6761b 100644 --- a/repository/src/main/java/org/alfresco/repo/domain/schema/script/DeleteNotExistsExecutor.java +++ b/repository/src/main/java/org/alfresco/repo/domain/schema/script/DeleteNotExistsExecutor.java @@ -73,7 +73,7 @@ public class DeleteNotExistsExecutor implements StatementExecutor private String sql; private int line; private File scriptFile; - private Properties globalProperties; + protected Properties globalProperties; protected boolean readOnly; protected int deleteBatchSize; @@ -220,6 +220,7 @@ public class DeleteNotExistsExecutor implements StatementExecutor { // Process batch primaryId = processPrimaryTableResultSet(primaryPrepStmt, secondaryPrepStmts, deletePrepStmt, deleteIds, primaryTableName, primaryColumnName, tableColumn); + connection.commit(); if (primaryId == null) { @@ -298,7 +299,6 @@ public class DeleteNotExistsExecutor implements StatementExecutor if (deleteIds.size() == deleteBatchSize) { deleteFromPrimaryTable(deletePrepStmt, deleteIds, primaryTableName); - connection.commit(); } if (!resultSet.next()) diff --git a/repository/src/main/java/org/alfresco/repo/domain/schema/script/DeleteNotExistsV3Executor.java b/repository/src/main/java/org/alfresco/repo/domain/schema/script/DeleteNotExistsV3Executor.java new file mode 100644 index 0000000000..62d5a4ed2e --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/domain/schema/script/DeleteNotExistsV3Executor.java @@ -0,0 +1,501 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.repo.domain.schema.script; + +import java.io.File; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Properties; +import java.util.Set; + +import javax.sql.DataSource; + +import org.alfresco.repo.domain.dialect.Dialect; +import org.alfresco.repo.domain.dialect.MySQLInnoDBDialect; +import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Same logic as DeleteNotExistsExecutor with the following changes: + *

+ * - filters the queries by unique values + *

+ * - eager close of result sets + *

+ * - we store all the ids in memory and process them from there - the secondary ids are stored in a unique list without + * duplicate values. + *

+ * - we only cross 2 sets (the potential ids to delete from the primary table with the set of all secondary ids in that + * range) removing all elements from the second set from the first set + *

+ * - every {pauseAndRecoverBatchSize} rows deleted we close all prepared statements and close the connection and sleep + * for {pauseAndRecoverTime} milliseconds. This is necessary to allow the DBMS to perform the background tasks without + * load from ACS. When we do not do this and if we are performing millions of deletes, the connection eventually gets + * aborted. + * + * @author Eva Vasques + */ +public class DeleteNotExistsV3Executor extends DeleteNotExistsExecutor +{ + private static Log logger = LogFactory.getLog(DeleteNotExistsV3Executor.class); + + public static final String PROPERTY_PAUSE_AND_RECOVER_BATCHSIZE = "system.delete_not_exists.pauseAndRecoverBatchSize"; + public static final String PROPERTY_PAUSE_AND_RECOVER_TIME = "system.delete_not_exists.pauseAndRecoverTime"; + public static final long DEFAULT_PAUSE_AND_RECOVER_BATCHSIZE = 500000; + public static final long DEFAULT_PAUSE_AND_RECOVER_TIME = 300000; + + private Dialect dialect; + private final DataSource dataSource; + private long pauseAndRecoverTime; + private long pauseAndRecoverBatchSize; + private boolean pauseAndRecover = false; + private int processedCounter = 0; + + public DeleteNotExistsV3Executor(Dialect dialect, Connection connection, String sql, int line, File scriptFile, + Properties globalProperties, DataSource dataSource) + { + super(connection, sql, line, scriptFile, globalProperties); + this.dialect = dialect; + this.dataSource = dataSource; + } + + @Override + public void execute() throws Exception + { + checkProperties(); + + String pauseAndRecoverBatchSizeString = globalProperties.getProperty(PROPERTY_PAUSE_AND_RECOVER_BATCHSIZE); + pauseAndRecoverBatchSize = pauseAndRecoverBatchSizeString == null ? DEFAULT_PAUSE_AND_RECOVER_BATCHSIZE + : Long.parseLong(pauseAndRecoverBatchSizeString); + + String pauseAndRecoverTimeString = globalProperties.getProperty(PROPERTY_PAUSE_AND_RECOVER_TIME); + pauseAndRecoverTime = pauseAndRecoverTimeString == null ? DEFAULT_PAUSE_AND_RECOVER_TIME + : Long.parseLong(pauseAndRecoverTimeString); + + super.execute(); + } + + @Override + protected void process(Pair[] tableColumn, Long[] tableUpperLimits, String[] optionalWhereClauses) + throws SQLException + { + String primaryTableName = tableColumn[0].getFirst(); + String primaryColumnName = tableColumn[0].getSecond(); + String primaryWhereClause = optionalWhereClauses[0]; + + Long primaryId = 0L; + + deletedCount = 0L; + startTime = new Date(); + + processBatch(primaryTableName, primaryColumnName, primaryWhereClause, primaryId, tableColumn, tableUpperLimits, + optionalWhereClauses); + + if (logger.isDebugEnabled()) + { + String msg = ((readOnly) ? "Script would have" : "Script") + " deleted a total of " + deletedCount + + " items from table " + primaryTableName + "."; + logger.debug(msg); + } + } + + private void processBatch(String primaryTableName, String primaryColumnName, String primaryWhereClause, Long primaryId, + Pair[] tableColumn, Long[] tableUpperLimits, String[] optionalWhereClauses) throws SQLException + { + PreparedStatement primaryPrepStmt = null; + PreparedStatement[] secondaryPrepStmts = null; + PreparedStatement deletePrepStmt = null; + Set deleteIds = new HashSet<>(); + pauseAndRecover = false; + + try + { + + connection.setAutoCommit(false); + + primaryPrepStmt = connection + .prepareStatement(createPreparedSelectStatement(primaryTableName, primaryColumnName, primaryWhereClause)); + primaryPrepStmt.setFetchSize(batchSize); + primaryPrepStmt.setLong(1, primaryId); + primaryPrepStmt.setLong(2, tableUpperLimits[0]); + + boolean hasResults = primaryPrepStmt.execute(); + + if (hasResults) + { + + // Prepared statements for secondary tables for the next batch + secondaryPrepStmts = new PreparedStatement[tableColumn.length]; + for (int i = 1; i < tableColumn.length; i++) + { + PreparedStatement secStmt = connection.prepareStatement(createPreparedSelectStatement( + tableColumn[i].getFirst(), tableColumn[i].getSecond(), optionalWhereClauses[i])); + secStmt.setFetchSize(batchSize); + secondaryPrepStmts[i] = secStmt; + } + + deletePrepStmt = connection.prepareStatement( + createPreparedDeleteStatement(primaryTableName, primaryColumnName, deleteBatchSize, primaryWhereClause)); + + // Timeout is only checked at each batch start. + // It can be further refined by being verified at each primary row processing. + while (hasResults && !isTimeoutExceeded()) + { + // Process batch + primaryId = processPrimaryTableResultSet(primaryPrepStmt, secondaryPrepStmts, deletePrepStmt, deleteIds, + primaryTableName, primaryColumnName, tableColumn); + connection.commit(); + + // If we have no more results (next primaryId is null) or job is marked for pause and recover, do + // not start the next batch + if (primaryId == null || pauseAndRecover) + { + break; + } + + // Prepare for next batch + primaryPrepStmt.setLong(1, primaryId + 1); + primaryPrepStmt.setLong(2, tableUpperLimits[0]); + + // Query the primary table for the next batch + hasResults = primaryPrepStmt.execute(); + } + + // Check if we have any more ids to delete + if (!deleteIds.isEmpty()) + { + deleteFromPrimaryTable(deletePrepStmt, deleteIds, primaryTableName); + connection.commit(); + } + } + } + finally + { + closeQuietly(deletePrepStmt); + closeQuietly(secondaryPrepStmts); + closeQuietly(primaryPrepStmt); + + closeQuietly(connection); + } + + if (pauseAndRecover) + { + pauseAndRecoverJob(dataSource); + logger.info("Resuming the job on primary table " + primaryTableName + " picking up after id " + primaryId); + processBatch(primaryTableName, primaryColumnName, primaryWhereClause, primaryId, tableColumn, tableUpperLimits, + optionalWhereClauses); + } + } + + @Override + protected String createPreparedSelectStatement(String tableName, String columnName, String whereClause) + { + StringBuilder sqlBuilder = new StringBuilder("SELECT " + columnName + " FROM " + tableName + " WHERE "); + + if (whereClause != null && !whereClause.isEmpty()) + { + sqlBuilder.append(whereClause + " AND "); + } + + sqlBuilder.append( + columnName + " >= ? AND " + columnName + " <= ? GROUP BY " + columnName + " ORDER BY " + columnName + " ASC "); + + if (dialect instanceof MySQLInnoDBDialect) + { + sqlBuilder.append(" LIMIT " + batchSize); + } + else + { + sqlBuilder.append(" OFFSET 0 ROWS FETCH FIRST " + batchSize + " ROWS ONLY"); + } + + return sqlBuilder.toString(); + } + + @Override + protected Long processPrimaryTableResultSet(PreparedStatement primaryPrepStmt, PreparedStatement[] secondaryPrepStmts, + PreparedStatement deletePrepStmt, Set deleteIds, String primaryTableName, String primaryColumnName, + Pair[] tableColumn) throws SQLException + { + Long primaryId = null; + Long minSecId = 0L; + Long maxSecId = 0L; + + // Set all rows retrieved from the primary table as our potential ids to delete + Set potentialIdsToDelete = new HashSet(); + Long minPotentialId = 0L; + Long maxPotentialId = 0L; + try (ResultSet resultSet = primaryPrepStmt.getResultSet()) + { + while (resultSet.next()) + { + primaryId = resultSet.getLong(primaryColumnName); + potentialIdsToDelete.add(primaryId); + + minPotentialId = (minPotentialId == 0L || primaryId < minPotentialId) ? primaryId : minPotentialId; + maxPotentialId = primaryId > maxPotentialId ? primaryId : maxPotentialId; + } + } + + if (potentialIdsToDelete.size() == 0) + { + // Nothing more to do + return primaryId; + } + + int rowsInBatch = potentialIdsToDelete.size(); + processedCounter = processedCounter + rowsInBatch; + + // Get a combined list of the ids present in the secondary tables + SecondaryResultsInfo secondaryResultsInfo = getSecondaryResults(secondaryPrepStmts, tableColumn, minPotentialId, + maxPotentialId); + + Set secondaryResults = secondaryResultsInfo.getValues(); + + if (secondaryResultsInfo.getSize() > 0) + { + minSecId = secondaryResultsInfo.getMinValue(); + maxSecId = secondaryResultsInfo.getMaxValue(); + + // From our potentialIdsToDelete list, remove all non-eligible ids: any id that is in a secondary table or + // any ID past the last ID we were able to access in the secondary tables (maxSecId) + Iterator it = potentialIdsToDelete.iterator(); + while (it.hasNext()) + { + Long id = it.next(); + if (id > maxSecId || secondaryResults.contains(id)) + { + it.remove(); + } + } + + // The next starting primary ID for the next batch will either be the next last id evaluated from the + // primary table or, in case the secondary queries did not get that far, the last secondary table id + // evaluated (maxSecId) + primaryId = primaryId < maxSecId ? primaryId : maxSecId; + } + + // Delete the ids that are eligble from the primary table + if (potentialIdsToDelete.size() > 0) + { + deleteInBatches(potentialIdsToDelete, deleteIds, primaryTableName, deletePrepStmt); + } + + if (logger.isTraceEnabled()) + { + logger.trace("Rows processed " + rowsInBatch + " from primary table " + primaryTableName + ". Primary: [" + + minPotentialId + "," + maxPotentialId + "] Secondary rows processed: " + secondaryResultsInfo.getSize() + + " [" + minSecId + "," + maxSecId + "] Total Deleted: " + deletedCount); + } + + // If the total rows processed from all batches so far is greater that the defined pauseAndRecoverBatchSize, + // mark the job to pause and recover after completing this batch + if (processedCounter >= pauseAndRecoverBatchSize) + { + pauseAndRecover = true; + } + + // Return the last primary id processed for the next batch + return primaryId; + } + + private void deleteInBatches(Set potentialIdsToDelete, Set deleteIds, String primaryTableName, + PreparedStatement deletePrepStmt) throws SQLException + { + Iterator potentialIdsIt = potentialIdsToDelete.iterator(); + while (potentialIdsIt.hasNext()) + { + Long idToDelete = (Long) potentialIdsIt.next(); + deleteIds.add(idToDelete); + + if (deleteIds.size() == deleteBatchSize) + { + deleteFromPrimaryTable(deletePrepStmt, deleteIds, primaryTableName); + } + } + } + + /* + * Get a combined list of the ids present in all the secondary tables + */ + private SecondaryResultsInfo getSecondaryResults(PreparedStatement[] preparedStatements, Pair[] tableColumn, + Long minPotentialId, Long maxPotentialId) throws SQLException + { + Set secondaryResultValues = new HashSet(); + Long lowestUpperValue = 0L; + for (int i = 1; i < preparedStatements.length; i++) + { + String columnId = tableColumn[i].getSecond(); + PreparedStatement secStmt = preparedStatements[i]; + secStmt.setLong(1, minPotentialId); + secStmt.setLong(2, maxPotentialId); + + // Execute the query on each secondary table + boolean secHasResults = secStmt.execute(); + if (secHasResults) + { + try (ResultSet secResultSet = secStmt.getResultSet()) + { + Long thisId = 0L; + Long resultSize = 0L; + Long upperValue = 0L; + while (secResultSet.next()) + { + resultSize++; + thisId = secResultSet.getLong(columnId); + + // Add to the list if it's not there yet + if (!secondaryResultValues.contains(thisId)) + { + secondaryResultValues.add(thisId); + } + + upperValue = thisId > upperValue ? thisId : upperValue; + } + + // Set the upper min value. We need to gather the last ID processed, so on the next batch on the + // primary table we can resume from there. We only need to do this if the number of results of the + // secondary table matches the batch size (if not, it means that there aren't more results up to + // maxPotentialId). Example on why this is needed: Primary table batch has 100k results from id's 1 + // to 250000. Secondary table on that interval returns 100k results from id 3 to 210000. Next batch + // needs to start on id 210001 + if (upperValue > 0 && resultSize == batchSize) + { + lowestUpperValue = (lowestUpperValue == 0L || upperValue < lowestUpperValue) ? upperValue + : lowestUpperValue; + } + } + } + } + + // If lowestUpperValue is still 0 (because a secondary table never had more or the same number of results as the + // primary table), the next id should be the last max id evaluated from the primary table (maxPotentialId) + lowestUpperValue = lowestUpperValue == 0 ? maxPotentialId : lowestUpperValue; + + // Remove all values after the lower upper value of a secondary table + long minSecId = 0L; + Iterator it = secondaryResultValues.iterator(); + while (it.hasNext()) + { + long secondaryId = it.next(); + if (secondaryId > lowestUpperValue) + { + it.remove(); + } + else + { + minSecId = (minSecId == 0L || secondaryId < minSecId) ? secondaryId : minSecId; + } + } + + // Return a combined list of the ids present in all the secondary tables + return new SecondaryResultsInfo(secondaryResultValues, minSecId, lowestUpperValue); + } + + private class SecondaryResultsInfo + { + Set values; + long minValue; + long maxValue; + long size; + + public SecondaryResultsInfo(Set values, long minValue, long maxValue) + { + super(); + this.values = values; + this.minValue = minValue; + this.maxValue = maxValue; + this.size = values.size(); + } + + public Set getValues() + { + return values; + } + + public long getMinValue() + { + return minValue; + } + + public long getMaxValue() + { + return maxValue; + } + + public long getSize() + { + return size; + } + } + + /* + * Sleep for {pauseAndRecoverTime} before opening a new connection and continue to process new batches + */ + private void pauseAndRecoverJob(DataSource dataSource) throws SQLException + { + if (logger.isDebugEnabled()) + { + logger.debug("Reached batch size for pause and recovery. Job will resume in " + pauseAndRecoverTime + " ms"); + } + // Wait + try + { + Thread.sleep(pauseAndRecoverTime); + } + catch (InterruptedException e) + { + // Do nothing + } + // Start another connection and continue where we left off + connection = dataSource.getConnection(); + pauseAndRecover = false; + processedCounter = 0; + } + + protected void closeQuietly(Connection connection) + { + try + { + connection.close(); + } + catch (SQLException e) + { + // Do nothing + } + finally + { + connection = null; + } + } +} \ No newline at end of file diff --git a/repository/src/main/java/org/alfresco/repo/domain/schema/script/MySQLDeleteNotExistsExecutor.java b/repository/src/main/java/org/alfresco/repo/domain/schema/script/MySQLDeleteNotExistsExecutor.java index bbe1920d1a..0f27502d6b 100644 --- a/repository/src/main/java/org/alfresco/repo/domain/schema/script/MySQLDeleteNotExistsExecutor.java +++ b/repository/src/main/java/org/alfresco/repo/domain/schema/script/MySQLDeleteNotExistsExecutor.java @@ -117,6 +117,7 @@ public class MySQLDeleteNotExistsExecutor extends DeleteNotExistsExecutor { // Process batch primaryId = processPrimaryTableResultSet(primaryPrepStmt, secondaryPrepStmts, deletePrepStmt, deleteIds, primaryTableName, primaryColumnName, tableColumn); + connection.commit(); if (primaryId == null) { @@ -195,7 +196,6 @@ public class MySQLDeleteNotExistsExecutor extends DeleteNotExistsExecutor if (deleteIds.size() == deleteBatchSize) { deleteFromPrimaryTable(deletePrepStmt, deleteIds, primaryTableName); - connection.commit(); } if (!resultSet.next()) @@ -207,13 +207,13 @@ public class MySQLDeleteNotExistsExecutor extends DeleteNotExistsExecutor primaryId = resultSet.getLong(primaryColumnName); } - if (logger.isTraceEnabled()) - { - logger.trace("RowsProcessed " + rowsProcessed + " from primary table " + primaryTableName); - } - updateSecondaryIds(primaryId, secondaryIds, secondaryPrepStmts, secondaryOffsets, secondaryResultSets, tableColumn); } + + if (logger.isTraceEnabled()) + { + logger.trace("RowsProcessed " + rowsProcessed + " from primary table " + primaryTableName); + } } finally { diff --git a/repository/src/main/java/org/alfresco/repo/domain/schema/script/ScriptExecutorImpl.java b/repository/src/main/java/org/alfresco/repo/domain/schema/script/ScriptExecutorImpl.java index 80ed84b726..eb8bce1160 100644 --- a/repository/src/main/java/org/alfresco/repo/domain/schema/script/ScriptExecutorImpl.java +++ b/repository/src/main/java/org/alfresco/repo/domain/schema/script/ScriptExecutorImpl.java @@ -274,6 +274,8 @@ public class ScriptExecutorImpl implements ScriptExecutor while(true) { + connection = refreshConnection(connection); + String sqlOriginal = reader.readLine(); line++; @@ -348,6 +350,24 @@ public class ScriptExecutorImpl implements ScriptExecutor } continue; } + else if (sql.startsWith("--DELETE_NOT_EXISTS_V3")) + { + DeleteNotExistsV3Executor deleteNotExistsFiltered = createDeleteNotExistV3Executor(dialect, connection, sql, + line, scriptFile); + deleteNotExistsFiltered.execute(); + + // Reset + sb.setLength(0); + fetchVarName = null; + fetchColumnName = null; + defaultFetchValue = null; + batchTableName = null; + doBatch = false; + batchUpperLimit = 0; + batchSize = 1; + + continue; + } else if (sql.startsWith("--DELETE_NOT_EXISTS")) { DeleteNotExistsExecutor deleteNotExists = createDeleteNotExistsExecutor(dialect, connection, sql, line, scriptFile); @@ -538,6 +558,17 @@ public class ScriptExecutorImpl implements ScriptExecutor } } + private Connection refreshConnection(Connection connection) throws SQLException + { + if (connection == null || connection.isClosed()) + { + connection = dataSource.getConnection(); + connection.setAutoCommit(true); + } + + return connection; + } + private DeleteNotExistsExecutor createDeleteNotExistsExecutor(Dialect dialect, Connection connection, String sql, int line, File scriptFile) { if (dialect instanceof MySQLInnoDBDialect) @@ -548,6 +579,12 @@ public class ScriptExecutorImpl implements ScriptExecutor return new DeleteNotExistsExecutor(connection, sql, line, scriptFile, globalProperties); } + private DeleteNotExistsV3Executor createDeleteNotExistV3Executor(Dialect dialect, Connection connection, String sql, int line, + File scriptFile) + { + return new DeleteNotExistsV3Executor(dialect, connection, sql, line, scriptFile, globalProperties, dataSource); + } + /** * Execute the given SQL statement, absorbing exceptions that we expect during * schema creation or upgrade. diff --git a/repository/src/main/java/org/alfresco/repo/download/CreateDownloadArchiveAction.java b/repository/src/main/java/org/alfresco/repo/download/CreateDownloadArchiveAction.java index 48f88eeb7e..cbfe5bbd00 100644 --- a/repository/src/main/java/org/alfresco/repo/download/CreateDownloadArchiveAction.java +++ b/repository/src/main/java/org/alfresco/repo/download/CreateDownloadArchiveAction.java @@ -1,28 +1,28 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.repo.download; import java.io.File; @@ -36,6 +36,7 @@ import org.alfresco.model.ForumModel; import org.alfresco.model.RenditionModel; import org.alfresco.repo.action.executer.ActionExecuter; import org.alfresco.repo.action.executer.ActionExecuterAbstractBase; +import org.alfresco.repo.rule.RuleModel; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.transaction.RetryingTransactionHelper; @@ -203,7 +204,7 @@ public class CreateDownloadArchiveAction extends ActionExecuterAbstractBase crawlerParameters.setExportFrom(exportFrom); crawlerParameters.setCrawlSelf(true); - crawlerParameters.setExcludeChildAssocs(new QName[] {RenditionModel.ASSOC_RENDITION, ForumModel.ASSOC_DISCUSSION}); + crawlerParameters.setExcludeChildAssocs(new QName[] {RenditionModel.ASSOC_RENDITION, ForumModel.ASSOC_DISCUSSION, RuleModel.ASSOC_RULE_FOLDER }); crawlerParameters.setExcludeAspects(new QName[] {ContentModel.ASPECT_WORKING_COPY}); // Get an estimate of the size for statuses diff --git a/repository/src/main/java/org/alfresco/repo/exporter/ExporterComponent.java b/repository/src/main/java/org/alfresco/repo/exporter/ExporterComponent.java index 352826b815..ebf1fcc99f 100644 --- a/repository/src/main/java/org/alfresco/repo/exporter/ExporterComponent.java +++ b/repository/src/main/java/org/alfresco/repo/exporter/ExporterComponent.java @@ -1,28 +1,28 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.repo.exporter; import java.io.IOException; @@ -39,6 +39,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.Arrays; import org.alfresco.model.ContentModel; import org.alfresco.repo.node.MLPropertyInterceptor; @@ -77,6 +78,7 @@ import org.alfresco.service.descriptor.DescriptorService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.RegexQNamePattern; +import org.apache.commons.lang3.math.NumberUtils; import org.dom4j.io.OutputFormat; import org.dom4j.io.XMLWriter; import org.springframework.extensions.surf.util.ParameterCheck; @@ -99,6 +101,8 @@ public class ExporterComponent private DescriptorService descriptorService; private AuthenticationService authenticationService; private PermissionService permissionService; + + private String exportChunkSize; /** Indent Size */ @@ -178,6 +182,14 @@ public class ExporterComponent { this.exportSecondaryNodes = exportSecondaryNodes; } + + /** + * @param exportChunkSize the exportChunkSize + */ + public void setExportChunkSize(String exportChunkSize) + { + this.exportChunkSize = exportChunkSize; + } /* (non-Javadoc) * @see org.alfresco.service.cmr.view.ExporterService#exportView(java.io.OutputStream, org.alfresco.service.cmr.view.ExporterCrawlerParameters, org.alfresco.service.cmr.view.Exporter) @@ -943,28 +955,23 @@ public class ExporterComponent try { // Current strategy is to determine if node is a child of the root exported node - for (NodeRef exportRoot : context.getExportList()) + if (context.getExportMap() != null) { - if (nodeRef.equals(exportRoot) && parameters.isCrawlSelf() == true) + for (NodeRef[] listNodeRef : context.getExportMap().values()) { - // node to export is the root export node (and root is to be exported) - isWithin = true; - } - else - { - // locate export root in primary parent path of node - Path nodePath = nodeService.getPath(nodeRef); - for (int i = nodePath.size() - 1; i >= 0; i--) + for (NodeRef exportRoot : listNodeRef) { - Path.ChildAssocElement pathElement = (Path.ChildAssocElement) nodePath.get(i); - if (pathElement.getRef().getChildRef().equals(exportRoot)) - { - isWithin = true; - break; - } + isWithin = checkIsWithin(nodeRef, exportRoot, parameters); } } } + else + { + for (NodeRef exportRoot : context.getExportList()) + { + isWithin = checkIsWithin(nodeRef, exportRoot, parameters); + } + } } catch (AccessDeniedException accessErr) { @@ -979,6 +986,28 @@ public class ExporterComponent } } + private boolean checkIsWithin(NodeRef nodeRef, NodeRef exportRoot, ExporterCrawlerParameters parameters){ + if (nodeRef.equals(exportRoot) && parameters.isCrawlSelf() == true) + { + // node to export is the root export node (and root is to be exported) + return true; + } + else + { + // locate export root in primary parent path of node + Path nodePath = nodeService.getPath(nodeRef); + for (int i = nodePath.size() - 1; i >= 0; i--) + { + Path.ChildAssocElement pathElement = (Path.ChildAssocElement) nodePath.get(i); + if (pathElement.getRef().getChildRef().equals(exportRoot)) + { + return true; + } + } + } + return false; + } + /** * Exporter Context @@ -986,7 +1015,9 @@ public class ExporterComponent private class ExporterContextImpl implements ExporterContext { private NodeRef[] exportList; + private Map exportListMap; private NodeRef[] parentList; + private Map parentListMap; private String exportedBy; private Date exportedDate; private String exporterVersion; @@ -995,8 +1026,10 @@ public class ExporterComponent private Map> nodesWithAssociations = new HashMap>(); private int index; - - + private int indexSubList; + private int chunkSize; + + /** * Construct * @@ -1005,7 +1038,17 @@ public class ExporterComponent public ExporterContextImpl(ExporterCrawlerParameters parameters) { index = 0; - + indexSubList = 0; + + + if(!NumberUtils.isParsable(exportChunkSize)){ + chunkSize = 10; + } + else + { + chunkSize = Integer.parseInt(exportChunkSize); + } + // get current user performing export String currentUserName = authenticationService.getCurrentUserName(); exportedBy = (currentUserName == null) ? "unknown" : currentUserName; @@ -1022,24 +1065,80 @@ public class ExporterComponent NodeRef exportOf = getNodeRef(parameters.getExportFrom()); exportList[0] = exportOf; } - parentList = new NodeRef[exportList.length]; - for (int i = 0; i < exportList.length; i++) + if(exportList.length > chunkSize) { - parentList[i] = getParent(exportList[i], parameters.isCrawlSelf()); + exportListMap = splitArray(exportList); + + parentListMap = new HashMap<>(); + for(Map.Entry exportEntrySet : exportListMap.entrySet()) + { + parentList= new NodeRef[exportEntrySet.getValue().length]; + for (int i = 0; i < exportEntrySet.getValue().length; i++) + { + parentList[i] = getParent(exportEntrySet.getValue()[i], parameters.isCrawlSelf()); + } + parentListMap.put(exportEntrySet.getKey(), parentList); + } } - + else{ + parentList = new NodeRef[exportList.length]; + for (int i = 0; i < exportList.length; i++) + { + parentList[i] = getParent(exportList[i], parameters.isCrawlSelf()); + } + } + // get exporter version exporterVersion = descriptorService.getServerDescriptor().getVersion(); } + + public Map splitArray(NodeRef[] arrayToSplit){ + if(chunkSize <= 0){ + return null; + } + int rest = arrayToSplit.length % chunkSize; + int chunks = arrayToSplit.length / chunkSize + (rest > 0 ? 1 : 0); + Map arrays = new HashMap<>() ; + for(Integer i = 0; i < (rest > 0 ? chunks - 1 : chunks); i++){ + arrays.put(i, Arrays.copyOfRange(arrayToSplit, i * chunkSize, i * chunkSize + chunkSize)); + } + if(rest > 0){ + arrays.put(chunks - 1, Arrays.copyOfRange(arrayToSplit, (chunks - 1) * chunkSize, (chunks - 1) * chunkSize + rest)); + } + return arrays; + } public boolean canRetrieve() { - return index < exportList.length; + if(exportListMap != null) + { + if (exportListMap.containsKey(indexSubList)) + { + return index < exportListMap.get(indexSubList).length; + } + else + { + return false; + } + } + else { + return index < exportList.length; + } } public int setNextValue() { - return ++index; + if(exportListMap != null && (index == exportListMap.get(indexSubList).length-1)){ + resetContext(); + if(indexSubList <= exportListMap.size()) + { + ++indexSubList; + } + } + else{ + ++index; + } + return index; } public void resetContext() @@ -1078,7 +1177,13 @@ public class ExporterComponent { if (canRetrieve()) { - return exportList[index]; + if(exportListMap!=null) + { + return exportListMap.get(indexSubList)[index]; + } + else { + return exportList[index]; + } } return null; } @@ -1091,7 +1196,13 @@ public class ExporterComponent { if (canRetrieve()) { - return parentList[index]; + if(parentListMap!=null) + { + return parentListMap.get(indexSubList)[index]; + } + else { + return parentList[index]; + } } return null; } @@ -1105,6 +1216,11 @@ public class ExporterComponent return exportList; } + public Map getExportMap() + { + return exportListMap; + } + /* * (non-Javadoc) * @see org.alfresco.service.cmr.view.ExporterContext#getExportParentList() diff --git a/repository/src/main/java/org/alfresco/repo/importer/ImporterComponent.java b/repository/src/main/java/org/alfresco/repo/importer/ImporterComponent.java index d377ace7fb..1cfc2d73c0 100644 --- a/repository/src/main/java/org/alfresco/repo/importer/ImporterComponent.java +++ b/repository/src/main/java/org/alfresco/repo/importer/ImporterComponent.java @@ -390,8 +390,12 @@ public class ImporterComponent implements ImporterService { String[] qnameComponents = QName.splitPrefixedQName(segments[i]); - String localName = QName.createValidLocalName(qnameComponents[1]); - + String localName = qnameComponents[1]; + if (localName == null || localName.length() == 0) + { + throw new IllegalArgumentException("Local name cannot be null or empty."); + } + // MT: bootstrap of "alfrescoUserStore.xml" requires 'sys:people/cm:admin@tenant' to be encoded as 'sys:people/cm:admin_x0040_tenant' (for XPath) localName = localName.replace("@", "_x0040_"); diff --git a/repository/src/main/java/org/alfresco/repo/jscript/RhinoScriptProcessor.java b/repository/src/main/java/org/alfresco/repo/jscript/RhinoScriptProcessor.java index 7f06ac4f95..dee9b5cffc 100644 --- a/repository/src/main/java/org/alfresco/repo/jscript/RhinoScriptProcessor.java +++ b/repository/src/main/java/org/alfresco/repo/jscript/RhinoScriptProcessor.java @@ -270,25 +270,33 @@ public class RhinoScriptProcessor extends BaseProcessor implements ScriptProcess */ public Object executeString(String source, Map model) { - try - { - // compile the script based on the node content - Script script; - Context cx = Context.enter(); - try - { - script = cx.compileString(resolveScriptImports(source), "AlfrescoJS", 1, null); - } - finally - { - Context.exit(); - } - return executeScriptImpl(script, model, true, "string script"); - } - catch (Throwable err) - { - throw new ScriptException("Failed to execute supplied script: " + err.getMessage(), err); - } + return executeString(source, model, false); + } + + /** + * @see org.alfresco.service.cmr.repository.ScriptProcessor#executeString(java.lang.String, java.util.Map, boolean) + */ + public Object executeString(String source, Map model, boolean secure) + { + try + { + // compile the script based on the node content + Script script; + Context cx = Context.enter(); + try + { + script = cx.compileString(resolveScriptImports(source), "AlfrescoJS", 1, null); + } + finally + { + Context.exit(); + } + return executeScriptImpl(script, model, secure, "string script"); + } + catch (Throwable err) + { + throw new ScriptException("Failed to execute supplied script: " + err.getMessage(), err); + } } /** diff --git a/repository/src/main/java/org/alfresco/repo/module/DeprecatedModulesValidator.java b/repository/src/main/java/org/alfresco/repo/module/DeprecatedModulesValidator.java new file mode 100644 index 0000000000..1a850a716c --- /dev/null +++ b/repository/src/main/java/org/alfresco/repo/module/DeprecatedModulesValidator.java @@ -0,0 +1,76 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.module; + +import static java.util.Optional.ofNullable; +import static java.util.function.Predicate.not; +import static java.util.stream.Collectors.joining; + +import java.util.List; + +import org.alfresco.service.cmr.module.ModuleDetails; +import org.alfresco.service.cmr.module.ModuleService; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * Halts the bootstrap process if a deprecated module is present. + * + * @author Domenico Sibilio + * @since 7.3.0 + */ +public class DeprecatedModulesValidator +{ + private static final String ERROR_MSG = "module.err.deprecated_modules"; + private final ModuleService moduleService; + private final List deprecatedModules; + + public DeprecatedModulesValidator(final ModuleService moduleService, final List deprecatedModules) + { + this.moduleService = moduleService; + this.deprecatedModules = deprecatedModules; + } + + public void onInit() + { + ofNullable(moduleService.getAllModules()) + .map(this::getDeprecatedModules) + .filter(not(String::isBlank)) + .ifPresent(DeprecatedModulesValidator::throwException); + } + + private String getDeprecatedModules(List modules) + { + return modules.stream() + .filter(module -> deprecatedModules.contains(module.getId())) + .map(module -> module.getTitle() + " " + module.getModuleVersionNumber()) + .collect(joining(", ")); + } + + private static void throwException(String foundDeprecatedModules) + { + throw new IllegalStateException(I18NUtil.getMessage(ERROR_MSG, foundDeprecatedModules)); + } +} diff --git a/repository/src/main/java/org/alfresco/repo/processor/ScriptServiceImpl.java b/repository/src/main/java/org/alfresco/repo/processor/ScriptServiceImpl.java index cfced0f9a4..a517bb7c81 100644 --- a/repository/src/main/java/org/alfresco/repo/processor/ScriptServiceImpl.java +++ b/repository/src/main/java/org/alfresco/repo/processor/ScriptServiceImpl.java @@ -188,6 +188,15 @@ public class ScriptServiceImpl implements ScriptService throws ScriptException { return executeScriptString(this.defaultScriptProcessor, script, model); + } + + /** + * @see org.alfresco.service.cmr.repository.ScriptService#executeScriptString(java.lang.String, java.util.Map, boolean) + */ + public Object executeScriptString(String script, Map model, boolean secure) + throws ScriptException + { + return executeScriptString(this.defaultScriptProcessor, script, model, secure); } /** @@ -197,12 +206,23 @@ public class ScriptServiceImpl implements ScriptService throws ScriptException { ScriptProcessor scriptProcessor = lookupScriptProcessor(engine); - return executeString(scriptProcessor, script, model); - } + return executeString(scriptProcessor, script, model, false); + } + + /** + * @see org.alfresco.service.cmr.repository.ScriptService#executeScriptString(java.lang.String, java.util.Map, boolean) + */ + public Object executeScriptString(String engine, String script, Map model, boolean secure) + throws ScriptException + { + ScriptProcessor scriptProcessor = lookupScriptProcessor(engine); + return executeString(scriptProcessor, script, model, secure); + } /** * Execute script - * + * + * @param processor the script processor that will be responsible for supplied script execution * @param location the location of the script * @param model context model * @return Object the result of the script @@ -227,6 +247,7 @@ public class ScriptServiceImpl implements ScriptService /** * Execute script * + * @param processor the script processor that will be responsible for supplied script execution * @param scriptRef the script node reference * @param contentProp the content property of the script * @param model the context model @@ -251,7 +272,8 @@ public class ScriptServiceImpl implements ScriptService /** * Execute script - * + * + * @param processor the script processor that will be responsible for supplied script execution * @param location the classpath string locating the script * @param model the context model * @return Object the result of the script @@ -275,12 +297,15 @@ public class ScriptServiceImpl implements ScriptService /** * Execute script string - * + * + * @param processor the script processor that will be responsible for supplied script execution * @param script the script string - * @param model the context model + * @param model the context model + * @param secure the flag indicating if string script is considered secure (e.g., if it comes from classpath) + * if true it will have access to the full execution context, if false the script will be executed in a sandbox context * @return Object the result of the script */ - protected Object executeString(ScriptProcessor processor, String script, Map model) + protected Object executeString(ScriptProcessor processor, String script, Map model, boolean secure) { ParameterCheck.mandatoryString("script", script); @@ -290,7 +315,7 @@ public class ScriptServiceImpl implements ScriptService } try { - return processor.executeString(script, model); + return processor.executeString(script, model, secure); } catch (Throwable err) { diff --git a/repository/src/main/java/org/alfresco/repo/rendition2/LocalSynchronousTransformClient.java b/repository/src/main/java/org/alfresco/repo/rendition2/LocalSynchronousTransformClient.java index ea09d9f510..4c6d1fb08e 100644 --- a/repository/src/main/java/org/alfresco/repo/rendition2/LocalSynchronousTransformClient.java +++ b/repository/src/main/java/org/alfresco/repo/rendition2/LocalSynchronousTransformClient.java @@ -33,7 +33,7 @@ import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.DirectAccessUrl; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.transform.client.model.config.CoreFunction; +import org.alfresco.transform.config.CoreFunction; import org.alfresco.util.PropertyCheck; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -43,7 +43,7 @@ import java.util.HashMap; import java.util.Map; import static org.alfresco.model.ContentModel.PROP_CONTENT; -import static org.alfresco.transform.client.util.RequestParamMap.DIRECT_ACCESS_URL; +import static org.alfresco.transform.common.RequestParamMap.DIRECT_ACCESS_URL; /** * Request synchronous transforms. diff --git a/repository/src/main/java/org/alfresco/repo/rendition2/LocalTransformClient.java b/repository/src/main/java/org/alfresco/repo/rendition2/LocalTransformClient.java index 852df49d8e..25f74714aa 100644 --- a/repository/src/main/java/org/alfresco/repo/rendition2/LocalTransformClient.java +++ b/repository/src/main/java/org/alfresco/repo/rendition2/LocalTransformClient.java @@ -35,7 +35,7 @@ import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.DirectAccessUrl; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.transaction.TransactionService; -import org.alfresco.transform.client.model.config.CoreFunction; +import org.alfresco.transform.config.CoreFunction; import org.alfresco.util.PropertyCheck; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -48,7 +48,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import static org.alfresco.model.ContentModel.PROP_CONTENT; -import static org.alfresco.transform.client.util.RequestParamMap.DIRECT_ACCESS_URL; +import static org.alfresco.transform.common.RequestParamMap.DIRECT_ACCESS_URL; /** * Requests rendition transforms take place using transforms available on the local machine (based on diff --git a/repository/src/main/java/org/alfresco/repo/rendition2/RenditionDefinitionRegistry2Impl.java b/repository/src/main/java/org/alfresco/repo/rendition2/RenditionDefinitionRegistry2Impl.java index f68fb4a4f1..61a3555016 100644 --- a/repository/src/main/java/org/alfresco/repo/rendition2/RenditionDefinitionRegistry2Impl.java +++ b/repository/src/main/java/org/alfresco/repo/rendition2/RenditionDefinitionRegistry2Impl.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2020 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -27,7 +27,7 @@ package org.alfresco.repo.rendition2; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.alfresco.transform.client.registry.TransformServiceRegistry; +import org.alfresco.transform.registry.TransformServiceRegistry; import org.alfresco.util.ConfigFileFinder; import org.alfresco.util.ConfigScheduler; import org.alfresco.util.Pair; diff --git a/repository/src/main/java/org/alfresco/repo/rendition2/SwitchingTransformServiceRegistry.java b/repository/src/main/java/org/alfresco/repo/rendition2/SwitchingTransformServiceRegistry.java index c9647621a7..38beac1769 100644 --- a/repository/src/main/java/org/alfresco/repo/rendition2/SwitchingTransformServiceRegistry.java +++ b/repository/src/main/java/org/alfresco/repo/rendition2/SwitchingTransformServiceRegistry.java @@ -25,8 +25,8 @@ */ package org.alfresco.repo.rendition2; -import org.alfresco.transform.client.model.config.CoreFunction; -import org.alfresco.transform.client.registry.TransformServiceRegistry; +import org.alfresco.transform.config.CoreFunction; +import org.alfresco.transform.registry.TransformServiceRegistry; import java.util.Map; diff --git a/repository/src/main/java/org/alfresco/repo/rule/RuleServiceImpl.java b/repository/src/main/java/org/alfresco/repo/rule/RuleServiceImpl.java index 0a108ab7e7..5340251328 100644 --- a/repository/src/main/java/org/alfresco/repo/rule/RuleServiceImpl.java +++ b/repository/src/main/java/org/alfresco/repo/rule/RuleServiceImpl.java @@ -25,6 +25,26 @@ */ package org.alfresco.repo.rule; +import static org.alfresco.model.ContentModel.ASSOC_CONTAINS; +import static org.alfresco.repo.rule.RuleModel.ASPECT_IGNORE_INHERITED_RULES; +import static org.alfresco.repo.rule.RuleModel.ASSOC_RULE_FOLDER; +import static org.alfresco.service.cmr.security.AccessStatus.ALLOWED; +import static org.alfresco.service.namespace.RegexQNamePattern.MATCH_ALL; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.Sets; + import org.alfresco.model.ContentModel; import org.alfresco.repo.action.ActionImpl; import org.alfresco.repo.action.ActionModel; @@ -55,7 +75,6 @@ import org.alfresco.service.cmr.rule.Rule; import org.alfresco.service.cmr.rule.RuleService; import org.alfresco.service.cmr.rule.RuleServiceException; import org.alfresco.service.cmr.rule.RuleType; -import org.alfresco.service.cmr.security.AccessStatus; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.QNamePattern; @@ -65,15 +84,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.extensions.surf.util.ParameterCheck; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - /** * Rule service implementation. *

@@ -255,7 +265,7 @@ public class RuleServiceImpl policyComponent.bindAssociationBehaviour( NodeServicePolicies.OnCreateChildAssociationPolicy.QNAME, RuleModel.ASPECT_RULES, - RuleModel.ASSOC_RULE_FOLDER, + ASSOC_RULE_FOLDER, new JavaBehaviour(this, "onCreateChildAssociation")); policyComponent.bindClassBehaviour( NodeServicePolicies.OnAddAspectPolicy.QNAME, @@ -347,8 +357,8 @@ public class RuleServiceImpl List assocs = this.runtimeNodeService.getChildAssocs( nodeRef, - RuleModel.ASSOC_RULE_FOLDER, - RuleModel.ASSOC_RULE_FOLDER); + ASSOC_RULE_FOLDER, + ASSOC_RULE_FOLDER); if (assocs.size() > 1) { throw new ActionServiceException("There is more than one rule folder, which is invalid."); @@ -479,7 +489,7 @@ public class RuleServiceImpl // Node has gone or is not the correct type return rules; } - if (includeInherited == true && runtimeNodeService.hasAspect(nodeRef, RuleModel.ASPECT_IGNORE_INHERITED_RULES) == false) + if (includeInherited && !runtimeNodeService.hasAspect(nodeRef, ASPECT_IGNORE_INHERITED_RULES)) { // Get any inherited rules for (Rule rule : getInheritedRules(nodeRef, ruleTypeName, null)) @@ -514,7 +524,7 @@ public class RuleServiceImpl // https://issues.alfresco.com/browse/ETWOTWO-438 if (!runtimeNodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) || - permissionService.hasPermission(nodeRef, PermissionService.READ) != AccessStatus.ALLOWED) + permissionService.hasPermission(nodeRef, PermissionService.READ) != ALLOWED) { // Doesn't have the aspect or the user doesn't have access return Collections.emptyList(); @@ -532,7 +542,7 @@ public class RuleServiceImpl { // Get the rules for this node List ruleChildAssocRefs = - this.runtimeNodeService.getChildAssocs(ruleFolder, RegexQNamePattern.MATCH_ALL, ASSOC_NAME_RULES_REGEX); + this.runtimeNodeService.getChildAssocs(ruleFolder, MATCH_ALL, ASSOC_NAME_RULES_REGEX); for (ChildAssociationRef ruleChildAssocRef : ruleChildAssocRefs) { // Create the rule and add to the list @@ -561,7 +571,7 @@ public class RuleServiceImpl { // Get the rules for this node List ruleChildAssocRefs = - this.runtimeNodeService.getChildAssocs(ruleFolder, RegexQNamePattern.MATCH_ALL, ASSOC_NAME_RULES_REGEX); + this.runtimeNodeService.getChildAssocs(ruleFolder, MATCH_ALL, ASSOC_NAME_RULES_REGEX); ruleCount = ruleChildAssocRefs.size(); } @@ -597,10 +607,106 @@ public class RuleServiceImpl return result; } - + + /** {@inheritDoc} */ + @Override + @Experimental + public List getNodesSupplyingRuleSets(NodeRef nodeRef) + { + return getNodesSupplyingRuleSets(nodeRef, new ArrayList<>()); + } + + /** + * Traverse the folder hierarchy find all the folder nodes that could supply rules by inheritance. + *

+ * The order of nodes returned by this methods has to match the order used by {@link #getInheritedRules}. + * + * @param nodeRef The starting node ref. + * @param visitedNodeRefs All the visited node refs (will be modified). + * @return A list of node refs, starting with the first parent of the first parent of ... and ending with the object generated by the + * given node ref. + */ + private List getNodesSupplyingRuleSets(NodeRef nodeRef, List visitedNodeRefs) + { + List returnList = new ArrayList<>(); + // This check prevents stack over flow when we have a cyclic node graph + if (!visitedNodeRefs.contains(nodeRef)) + { + visitedNodeRefs.add(nodeRef); + if (!runtimeNodeService.hasAspect(nodeRef, ASPECT_IGNORE_INHERITED_RULES)) + { + List parents = runtimeNodeService.getParentAssocs(nodeRef); + for (ChildAssociationRef parent : parents) + { + // We are not interested in following potentially massive person group membership trees! + if (!IGNORE_PARENT_ASSOC_TYPES.contains(parent.getTypeQName())) + { + // Update visitedNodeRefs with all the ancestors. + returnList.addAll(getNodesSupplyingRuleSets(parent.getParentRef(), visitedNodeRefs)); + } + } + } + returnList.add(nodeRef); + } + return returnList; + } + + /** {@inheritDoc} */ + @Override + @Experimental + public List getFoldersInheritingRuleSet(NodeRef ruleSet, int maxFoldersToReturn) + { + // Seed stack with all folders owning or linking to the rule set. + Deque stack = new LinkedList<>(); + for (ChildAssociationRef parentAssociation : runtimeNodeService.getParentAssocs(ruleSet)) + { + stack.add(parentAssociation.getParentRef()); + } + // Process child folders to find all that inherit the rules. + List inheritors = new ArrayList<>(); + while (!stack.isEmpty() && inheritors.size() < maxFoldersToReturn) + { + NodeRef folder = stack.pop(); + runtimeNodeService.getChildAssocs(folder, ASSOC_CONTAINS, MATCH_ALL).stream().map(ChildAssociationRef::getChildRef).forEach(childNode -> { + QName childType = runtimeNodeService.getType(childNode); + if (dictionaryService.isSubClass(childType, ContentModel.TYPE_FOLDER) + && !runtimeNodeService.hasAspect(childNode, ASPECT_IGNORE_INHERITED_RULES)) + { + stack.add(childNode); + // Only return nodes that the user has permission to view. + if (permissionService.hasReadPermission(childNode) == ALLOWED) + { + inheritors.add(childNode); + if (inheritors.size() == maxFoldersToReturn) + { + // Return once we've hit the limit. + return; + } + } + } + }); + } + return inheritors; + } + + /** {@inheritDoc} */ + @Override + @Experimental + public List getFoldersLinkingToRuleSet(NodeRef ruleSet, int maxFoldersToReturn) + { + NodeRef parentRef = nodeService.getPrimaryParent(ruleSet).getParentRef(); + return nodeService.getParentAssocs(ruleSet) + .stream() + .limit(maxFoldersToReturn) + .map(ChildAssociationRef::getParentRef) + .filter(folder -> !folder.equals(parentRef)) + .filter(folder -> permissionService.hasReadPermission(folder) == ALLOWED) + .collect(Collectors.toList()); + } + /** * Gets the inherited rules for a given node reference - * + * * @param nodeRef the nodeRef * @param ruleTypeName the rule type (null if all applicable) * @return a list of inherited rules (empty if none) @@ -608,20 +714,20 @@ public class RuleServiceImpl private List getInheritedRules(NodeRef nodeRef, String ruleTypeName, Set visitedNodeRefs) { List inheritedRules = new ArrayList(); - + if (this.runtimeNodeService.hasAspect(nodeRef, RuleModel.ASPECT_IGNORE_INHERITED_RULES) == false) - { + { // Create the visited nodes set if it has not already been created if (visitedNodeRefs == null) { visitedNodeRefs = new HashSet(); } - + // This check prevents stack over flow when we have a cyclic node graph if (visitedNodeRefs.contains(nodeRef) == false) { visitedNodeRefs.add(nodeRef); - + List allInheritedRules = new ArrayList(); List parents = this.runtimeNodeService.getParentAssocs(nodeRef); for (ChildAssociationRef parent : parents) @@ -641,7 +747,7 @@ public class RuleServiceImpl allInheritedRules.add(rule); } } - + List rules = getRules(parent.getParentRef(), false); for (Rule rule : rules) { @@ -652,7 +758,7 @@ public class RuleServiceImpl } } } - + if (ruleTypeName == null) { inheritedRules = allInheritedRules; @@ -670,7 +776,7 @@ public class RuleServiceImpl } } } - + return inheritedRules; } @@ -745,62 +851,63 @@ public class RuleServiceImpl } @Override - public void saveRule(NodeRef nodeRef, Rule rule) + public Rule saveRule(NodeRef nodeRef, Rule rule) { checkForLinkedRules(nodeRef); - if (this.permissionService.hasPermission(nodeRef, PermissionService.CHANGE_PERMISSIONS) == AccessStatus.ALLOWED) - { - disableRules(); - try - { - if (this.nodeService.exists(nodeRef) == false) - { - throw new RuleServiceException("The node does not exist."); - } - - NodeRef ruleNodeRef = rule.getNodeRef(); - if (ruleNodeRef == null) - { - if (this.nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == false) - { - // Add the actionable aspect - this.nodeService.addAspect(nodeRef, RuleModel.ASPECT_RULES, null); - } - - // Create the action node - ruleNodeRef = this.nodeService.createNode( - getSavedRuleFolderRef(nodeRef), - ContentModel.ASSOC_CONTAINS, - QName.createQName(RuleModel.RULE_MODEL_URI, ASSOC_NAME_RULES_PREFIX + GUID.generate()), - RuleModel.TYPE_RULE).getChildRef(); - - // Set the rule node reference and the owning node reference - rule.setNodeRef(ruleNodeRef); - } - - // Update the properties of the rule - this.nodeService.setProperty(ruleNodeRef, ContentModel.PROP_TITLE, rule.getTitle()); - this.nodeService.setProperty(ruleNodeRef, ContentModel.PROP_DESCRIPTION, rule.getDescription()); - this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_RULE_TYPE, (Serializable)rule.getRuleTypes()); - this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_APPLY_TO_CHILDREN, rule.isAppliedToChildren()); - this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_EXECUTE_ASYNC, rule.getExecuteAsynchronously()); - this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_DISABLED, rule.getRuleDisabled()); - - // Save the rule's action - saveAction(ruleNodeRef, rule); - } - finally - { - enableRules(); - // Drop the rules from the cache - nodeRulesCache.remove(nodeRef); - } - } - else + if (this.permissionService.hasPermission(nodeRef, PermissionService.CHANGE_PERMISSIONS) != ALLOWED) { throw new RuleServiceException("Insufficient permissions to save a rule."); } + + disableRules(); + try + { + if (this.nodeService.exists(nodeRef) == false) + { + throw new RuleServiceException("The node does not exist."); + } + + NodeRef ruleNodeRef = rule.getNodeRef(); + if (ruleNodeRef == null) + { + if (this.nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == false) + { + // Add the actionable aspect + this.nodeService.addAspect(nodeRef, RuleModel.ASPECT_RULES, null); + } + + // Create the action node + ruleNodeRef = this.nodeService.createNode( + getSavedRuleFolderRef(nodeRef), + ASSOC_CONTAINS, + QName.createQName(RuleModel.RULE_MODEL_URI, ASSOC_NAME_RULES_PREFIX + GUID.generate()), + RuleModel.TYPE_RULE).getChildRef(); + + // Set the rule node reference and the owning node reference + rule.setNodeRef(ruleNodeRef); + } + + // Update the properties of the rule + String title = rule.getTitle(); + ParameterCheck.mandatoryString("Rule name", title); + this.nodeService.setProperty(ruleNodeRef, ContentModel.PROP_TITLE, title); + this.nodeService.setProperty(ruleNodeRef, ContentModel.PROP_DESCRIPTION, rule.getDescription()); + this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_RULE_TYPE, (Serializable)rule.getRuleTypes()); + this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_APPLY_TO_CHILDREN, rule.isAppliedToChildren()); + this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_EXECUTE_ASYNC, rule.getExecuteAsynchronously()); + this.nodeService.setProperty(ruleNodeRef, RuleModel.PROP_DISABLED, rule.getRuleDisabled()); + + // Save the rule's action + saveAction(ruleNodeRef, rule); + } + finally + { + enableRules(); + // Drop the rules from the cache + nodeRulesCache.remove(nodeRef); + } + return rule; } @Override @@ -816,7 +923,7 @@ public class RuleServiceImpl NodeRef ruleFolder = getSavedRuleFolderRef(nodeRef); if (ruleFolder != null) { - List assocs = this.runtimeNodeService.getChildAssocs(ruleFolder, RegexQNamePattern.MATCH_ALL, ASSOC_NAME_RULES_REGEX); + List assocs = this.runtimeNodeService.getChildAssocs(ruleFolder, MATCH_ALL, ASSOC_NAME_RULES_REGEX); List orderedAssocs = new ArrayList(assocs.size()); ChildAssociationRef movedAssoc = null; for (ChildAssociationRef assoc : assocs) @@ -861,10 +968,7 @@ public class RuleServiceImpl { // Get the action definition from the rule Action action = rule.getAction(); - if (action == null) - { - throw new RuleServiceException("An action must be specified when defining a rule."); - } + ParameterCheck.mandatory("Rule action", action); // Get the current action node reference NodeRef actionNodeRef = null; @@ -901,10 +1005,9 @@ public class RuleServiceImpl { checkForLinkedRules(nodeRef); - if (this.permissionService.hasPermission(nodeRef, PermissionService.CHANGE_PERMISSIONS) == AccessStatus.ALLOWED) + if (permissionService.hasPermission(nodeRef, PermissionService.CHANGE_PERMISSIONS) == ALLOWED) { - if (this.nodeService.exists(nodeRef) == true && - this.nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == true) + if (nodeService.exists(nodeRef) && nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES)) { disableRules(nodeRef); try @@ -912,7 +1015,7 @@ public class RuleServiceImpl NodeRef ruleNodeRef = rule.getNodeRef(); if (ruleNodeRef != null) { - this.nodeService.removeChild(getSavedRuleFolderRef(nodeRef), ruleNodeRef); + nodeService.removeChild(getSavedRuleFolderRef(nodeRef), ruleNodeRef); } } finally @@ -935,7 +1038,7 @@ public class RuleServiceImpl } } - this.nodeService.removeAspect(nodeRef, RuleModel.ASPECT_RULES); + nodeService.removeAspect(nodeRef, RuleModel.ASPECT_RULES); } } // Drop the rules from the cache @@ -945,8 +1048,8 @@ public class RuleServiceImpl { throw new RuleServiceException("Insufficient permissions to remove a rule."); } - } - + } + /** * Checks if rules are linked and throws an exception if they are. * @@ -965,7 +1068,7 @@ public class RuleServiceImpl { checkForLinkedRules(nodeRef); - if (this.permissionService.hasPermission(nodeRef, PermissionService.CHANGE_PERMISSIONS) == AccessStatus.ALLOWED) + if (this.permissionService.hasPermission(nodeRef, PermissionService.CHANGE_PERMISSIONS) == ALLOWED) { if (this.nodeService.exists(nodeRef) == true && this.nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == true) @@ -975,7 +1078,7 @@ public class RuleServiceImpl { List ruleChildAssocs = this.nodeService.getChildAssocs( folder, - RegexQNamePattern.MATCH_ALL, ASSOC_NAME_RULES_REGEX); + MATCH_ALL, ASSOC_NAME_RULES_REGEX); for (ChildAssociationRef ruleChildAssoc : ruleChildAssocs) { this.nodeService.removeChild(folder, ruleChildAssoc.getChildRef()); @@ -1325,7 +1428,7 @@ public class RuleServiceImpl { boolean result = true; if (this.nodeService.exists(actionedUponNodeRef) - && this.permissionService.hasPermission(actionedUponNodeRef, PermissionService.READ).equals(AccessStatus.ALLOWED)) + && this.permissionService.hasPermission(actionedUponNodeRef, PermissionService.READ).equals(ALLOWED)) { NodeRef copiedFrom = copyService.getOriginal(actionedUponNodeRef); if (logger.isDebugEnabled() == true) @@ -1526,6 +1629,12 @@ public class RuleServiceImpl return this.nodeService.getPrimaryParent(systemFolder).getParentRef(); } + @Override + public NodeRef getOwningNodeRef(NodeRef ruleSet) + { + return nodeService.getPrimaryParent(ruleSet).getParentRef(); + } + @Override public NodeRef getOwningNodeRef(final Action action) { @@ -1595,7 +1704,7 @@ public class RuleServiceImpl public List getLinkedFromRuleNodes(NodeRef nodeRef) { List result = new ArrayList(); - + if (nodeService.hasAspect(nodeRef, RuleModel.ASPECT_RULES) == true) { ChildAssociationRef assoc = getSavedRuleFolderAssoc(nodeRef); @@ -1616,13 +1725,9 @@ public class RuleServiceImpl @Override @Experimental - public NodeRef getRuleSetNode(final NodeRef folderNodeRef) { - return getPrimaryChildNode(folderNodeRef, RuleModel.ASSOC_RULE_FOLDER); - } - - private NodeRef getPrimaryChildNode(final NodeRef nodeRef, final QNamePattern associationType) { - return runtimeNodeService.getChildAssocs(nodeRef, associationType, associationType).stream() - .filter(ChildAssociationRef::isPrimary) + public NodeRef getRuleSetNode(final NodeRef folderNodeRef) + { + return runtimeNodeService.getChildAssocs(folderNodeRef, ASSOC_RULE_FOLDER, ASSOC_RULE_FOLDER).stream() .map(ChildAssociationRef::getChildRef) .findFirst() .orElse(null); @@ -1630,17 +1735,23 @@ public class RuleServiceImpl @Override @Experimental - public boolean isRuleSetAssociatedWithFolder(final NodeRef ruleSetNodeRef, final NodeRef folderNodeRef) { - return isChildOf(ruleSetNodeRef, RuleModel.ASSOC_RULE_FOLDER, folderNodeRef); + public boolean isRuleSetAssociatedWithFolder(final NodeRef ruleSetNodeRef, final NodeRef folderNodeRef) + { + List associations = runtimeNodeService.getParentAssocs(ruleSetNodeRef, ASSOC_RULE_FOLDER, ASSOC_RULE_FOLDER); + Set associatedFolders = associations.stream().map(ChildAssociationRef::getParentRef).collect(Collectors.toSet()); + Set supplyingFolders = new HashSet<>(getNodesSupplyingRuleSets(folderNodeRef)); + return !Sets.intersection(associatedFolders, supplyingFolders).isEmpty(); } @Override @Experimental - public boolean isRuleAssociatedWithRuleSet(final NodeRef ruleNodeRef, final NodeRef ruleSetNodeRef) { + public boolean isRuleAssociatedWithRuleSet(final NodeRef ruleNodeRef, final NodeRef ruleSetNodeRef) + { return isChildOf(ruleNodeRef, null, ruleSetNodeRef); } - private boolean isChildOf(final NodeRef childNodeRef, final QNamePattern associationType, final NodeRef parentNodeRef) { + private boolean isChildOf(final NodeRef childNodeRef, final QNamePattern associationType, final NodeRef parentNodeRef) + { final List associations; if (associationType == null) { associations = runtimeNodeService.getParentAssocs(childNodeRef); @@ -1652,4 +1763,12 @@ public class RuleServiceImpl .map(ChildAssociationRef::getParentRef) .anyMatch(parentNodeRef::equals); } + + @Override + @Experimental + public boolean isRuleSetShared(final NodeRef ruleSetNodeRef) + { + return runtimeNodeService.getParentAssocs(ruleSetNodeRef).stream() + .anyMatch(association -> !association.isPrimary()); + } } diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/ldap/LDAPInitialDirContextFactoryImpl.java b/repository/src/main/java/org/alfresco/repo/security/authentication/ldap/LDAPInitialDirContextFactoryImpl.java index fc95211826..4bef18e41e 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/ldap/LDAPInitialDirContextFactoryImpl.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/ldap/LDAPInitialDirContextFactoryImpl.java @@ -25,54 +25,54 @@ */ package org.alfresco.repo.security.authentication.ldap; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.util.Collections; -import java.util.HashSet; -import java.util.Hashtable; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import javax.naming.AuthenticationNotSupportedException; -import javax.naming.CommunicationException; -import javax.naming.Context; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttribute; -import javax.naming.directory.BasicAttributes; -import javax.naming.directory.DirContext; -import javax.naming.directory.InitialDirContext; -import javax.naming.ldap.Control; -import javax.naming.ldap.InitialLdapContext; -import javax.naming.ldap.LdapContext; -import javax.naming.ldap.PagedResultsControl; -import javax.naming.ldap.PagedResultsResponseControl; - -import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.repo.security.authentication.AlfrescoSSLSocketFactory; -import org.alfresco.repo.security.authentication.AuthenticationDiagnostic; -import org.alfresco.repo.security.authentication.AuthenticationException; -import org.alfresco.util.ApplicationContextHelper; -import org.alfresco.util.PropertyCheck; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.InitializingBean; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import javax.naming.AuthenticationNotSupportedException; +import javax.naming.CommunicationException; +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttribute; +import javax.naming.directory.BasicAttributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import javax.naming.ldap.Control; +import javax.naming.ldap.InitialLdapContext; +import javax.naming.ldap.LdapContext; +import javax.naming.ldap.PagedResultsControl; +import javax.naming.ldap.PagedResultsResponseControl; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.repo.security.authentication.AlfrescoSSLSocketFactory; +import org.alfresco.repo.security.authentication.AuthenticationDiagnostic; +import org.alfresco.repo.security.authentication.AuthenticationException; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.PropertyCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; public class LDAPInitialDirContextFactoryImpl implements LDAPInitialDirContextFactory, InitializingBean { private static final Log logger = LogFactory.getLog(LDAPInitialDirContextFactoryImpl.class); - private static Set> checkedEnvs = Collections.synchronizedSet(new HashSet>( - 11)); + private SimpleCache>> ldapInitialDirContextCache; private Map defaultEnvironment = Collections. emptyMap(); private Map authenticatedEnvironment = Collections. emptyMap(); @@ -80,6 +80,13 @@ public class LDAPInitialDirContextFactoryImpl implements LDAPInitialDirContextFa private String trustStorePath; private String trustStoreType; private String trustStorePassPhrase; + + private boolean initialChecksEnabled = true; + + private final String ANONYMOUS_CHECK = "anonymous_check"; + private final String SIMPLE_DN_CHECK = "simple_dn_check"; + private final String DN_CHECK = "dn_check"; + private final String PRINCIPAL_CHECK = "principal_check"; public String getTrustStorePath() { @@ -475,159 +482,202 @@ public class LDAPInitialDirContextFactoryImpl implements LDAPInitialDirContextFa } - public void afterPropertiesSet() throws Exception - { - logger.debug("after Properties Set"); - // Check Anonymous bind - - Hashtable env = new Hashtable(authenticatedEnvironment.size()); - env.putAll(authenticatedEnvironment); - env.remove(Context.SECURITY_PRINCIPAL); - env.remove(Context.SECURITY_CREDENTIALS); - if (isSSLSocketFactoryRequired()) - { - KeyStore trustStore = initTrustStore(); - AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore); - env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName()); - } - try - { - new InitialDirContext(env); - - logger.warn("LDAP server supports anonymous bind " + env.get(Context.PROVIDER_URL)); - } - catch (javax.naming.AuthenticationException ax) - { - - } - catch (AuthenticationNotSupportedException e) - { - - } - catch (NamingException nx) - { - logger.error("Unable to connect to LDAP Server; check LDAP configuration", nx); - return; - } - - // Simple DN and password - - env = new Hashtable(authenticatedEnvironment.size()); - env.putAll(authenticatedEnvironment); - env.put(Context.SECURITY_PRINCIPAL, "daftAsABrush"); - env.put(Context.SECURITY_CREDENTIALS, "daftAsABrush"); - if (isSSLSocketFactoryRequired()) - { - KeyStore trustStore = initTrustStore(); - AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore); - env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName()); - } - try - { - - new InitialDirContext(env); - - throw new AuthenticationException( - "The ldap server at " - + env.get(Context.PROVIDER_URL) - + " falls back to use anonymous bind if invalid security credentials are presented. This is not supported."); - } - catch (javax.naming.AuthenticationException ax) - { - logger.info("LDAP server does not fall back to anonymous bind for a string uid and password at " + env.get(Context.PROVIDER_URL)); - } - catch (AuthenticationNotSupportedException e) - { - logger.info("LDAP server does not fall back to anonymous bind for a string uid and password at " + env.get(Context.PROVIDER_URL)); - } - catch (NamingException nx) - { - logger.info("LDAP server does not support simple string user ids and invalid credentials at "+ env.get(Context.PROVIDER_URL)); - } - - // DN and password - - env = new Hashtable(authenticatedEnvironment.size()); - env.putAll(authenticatedEnvironment); - env.put(Context.SECURITY_PRINCIPAL, "cn=daftAsABrush,dc=woof"); - env.put(Context.SECURITY_CREDENTIALS, "daftAsABrush"); - if (isSSLSocketFactoryRequired()) - { - KeyStore trustStore = initTrustStore(); - AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore); - env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName()); - } - try - { - - new InitialDirContext(env); - - throw new AuthenticationException( - "The ldap server at " - + env.get(Context.PROVIDER_URL) - + " falls back to use anonymous bind if invalid security credentials are presented. This is not supported."); - } - catch (javax.naming.AuthenticationException ax) - { - logger.info("LDAP server does not fall back to anonymous bind for a simple dn and password at " + env.get(Context.PROVIDER_URL)); - } - catch (AuthenticationNotSupportedException e) - { - logger.info("LDAP server does not fall back to anonymous bind for a simple dn and password at " + env.get(Context.PROVIDER_URL)); - } - catch (NamingException nx) - { - logger.info("LDAP server does not support simple DN and invalid password at "+ env.get(Context.PROVIDER_URL)); - } - - // Check more if we have a real principal we expect to work - - String principal = defaultEnvironment.get(Context.SECURITY_PRINCIPAL); - if (principal != null) - { - // Correct principal invalid password - - env = new Hashtable(authenticatedEnvironment.size()); - env.putAll(authenticatedEnvironment); - env.put(Context.SECURITY_PRINCIPAL, principal); - env.put(Context.SECURITY_CREDENTIALS, "sdasdasdasdasd123123123"); - if (isSSLSocketFactoryRequired()) - { - KeyStore trustStore = initTrustStore(); - AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore); - env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName()); - } - if (!checkedEnvs.contains(env)) - { - - try - { - - new InitialDirContext(env); - - throw new AuthenticationException( - "The ldap server at " - + env.get(Context.PROVIDER_URL) - + " falls back to use anonymous bind for a known principal if invalid security credentials are presented. This is not supported."); - } - catch (javax.naming.AuthenticationException ax) - { - logger.info("LDAP server does not fall back to anonymous bind for known principal and invalid credentials at " + env.get(Context.PROVIDER_URL)); - } - catch (AuthenticationNotSupportedException e) - { - logger.info("LDAP server does not support the required authentication mechanism"); - } - catch (NamingException nx) - { - // already done - } - // Record this environment as checked so that we don't check it again on further restarts / other subsystem - // instances - checkedEnvs.add(env); - } - } + public void afterPropertiesSet() throws Exception + { + logger.debug("after Properties Set"); + + if (initialChecksEnabled) + { + checkAnonymousBind(); + checkSimpleDnAndPassword(); + checkDnAndPassword(); + checkPrincipal(); + } + else + { + logger.info("LDAP checks are disabled"); + } } + + /** + * Check Anonymous bind + */ + private void checkAnonymousBind() + { + Hashtable env = new Hashtable(authenticatedEnvironment.size()); + env.putAll(authenticatedEnvironment); + env.remove(Context.SECURITY_PRINCIPAL); + env.remove(Context.SECURITY_CREDENTIALS); + + if (isSSLSocketFactoryRequired()) + { + KeyStore trustStore = initTrustStore(); + AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore); + env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName()); + } + + if (!isCached(ANONYMOUS_CHECK, env)) + { + logger.debug("Starting check: Anonymous bind"); + + try + { + new InitialDirContext(env); + + logger.warn("LDAP server supports anonymous bind " + env.get(Context.PROVIDER_URL)); + } + catch (javax.naming.AuthenticationException | AuthenticationNotSupportedException e) + { + // do nothing + } + catch (NamingException nx) + { + logger.error("Unable to connect to LDAP Server; check LDAP configuration", nx); + } + + addToCache(ANONYMOUS_CHECK, env); + } + } + + /** + * Check simple DN and password + */ + private void checkSimpleDnAndPassword() + { + Hashtable env = new Hashtable(authenticatedEnvironment.size()); + env.putAll(authenticatedEnvironment); + env.put(Context.SECURITY_PRINCIPAL, "daftAsABrush"); + env.put(Context.SECURITY_CREDENTIALS, "daftAsABrush"); + + if (isSSLSocketFactoryRequired()) + { + KeyStore trustStore = initTrustStore(); + AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore); + env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName()); + } + + if (!isCached(SIMPLE_DN_CHECK, env)) + { + logger.debug("Starting check: Simple DN and Password"); + + try + { + new InitialDirContext(env); + + throw new AuthenticationException("The ldap server at " + env.get(Context.PROVIDER_URL) + + " falls back to use anonymous bind if invalid security credentials are presented. This is not supported."); + } + catch (javax.naming.AuthenticationException | AuthenticationNotSupportedException e) + { + logger.info("LDAP server does not fall back to anonymous bind for a string uid and password at " + + env.get(Context.PROVIDER_URL)); + } + catch (NamingException nx) + { + logger.info("LDAP server does not support simple string user ids and invalid credentials at " + + env.get(Context.PROVIDER_URL)); + } + + addToCache(SIMPLE_DN_CHECK, env); + } + } + + /** + * Check DN and Password + */ + private void checkDnAndPassword() + { + Hashtable env = new Hashtable(authenticatedEnvironment.size()); + env.putAll(authenticatedEnvironment); + env.put(Context.SECURITY_PRINCIPAL, "cn=daftAsABrush,dc=woof"); + env.put(Context.SECURITY_CREDENTIALS, "daftAsABrush"); + + if (isSSLSocketFactoryRequired()) + { + KeyStore trustStore = initTrustStore(); + AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore); + env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName()); + } + + if (!isCached(DN_CHECK, env)) + { + logger.debug("Starting check: DN and Password"); + + try + { + new InitialDirContext(env); + + throw new AuthenticationException("The ldap server at " + env.get(Context.PROVIDER_URL) + + " falls back to use anonymous bind if invalid security credentials are presented. This is not supported."); + } + catch (javax.naming.AuthenticationException | AuthenticationNotSupportedException e) + { + logger.info("LDAP server does not fall back to anonymous bind for a simple dn and password at " + + env.get(Context.PROVIDER_URL)); + } + catch (NamingException nx) + { + logger.info("LDAP server does not support simple DN and invalid password at " + env.get(Context.PROVIDER_URL)); + } + + addToCache(DN_CHECK, env); + } + } + + /** + * Check more if we have a real principal we expect to work + */ + private void checkPrincipal() + { + String principal = defaultEnvironment.get(Context.SECURITY_PRINCIPAL); + + if (principal != null) + { + // Correct principal invalid password + + Hashtable env = new Hashtable(authenticatedEnvironment.size()); + env.putAll(authenticatedEnvironment); + env.put(Context.SECURITY_PRINCIPAL, principal); + env.put(Context.SECURITY_CREDENTIALS, "sdasdasdasdasd123123123"); + + if (isSSLSocketFactoryRequired()) + { + KeyStore trustStore = initTrustStore(); + AlfrescoSSLSocketFactory.initTrustedSSLSocketFactory(trustStore); + env.put("java.naming.ldap.factory.socket", AlfrescoSSLSocketFactory.class.getName()); + } + + if (!isCached(PRINCIPAL_CHECK, env)) + { + logger.debug("Starting check: Principal"); + + try + { + new InitialDirContext(env); + + throw new AuthenticationException("The ldap server at " + env.get(Context.PROVIDER_URL) + + " falls back to use anonymous bind for a known principal if invalid security credentials are presented. This is not supported."); + } + catch (javax.naming.AuthenticationException ax) + { + logger.info("LDAP server does not fall back to anonymous bind for known principal and invalid credentials at " + + env.get(Context.PROVIDER_URL)); + } + catch (AuthenticationNotSupportedException e) + { + logger.info("LDAP server does not support the required authentication mechanism"); + } + catch (NamingException nx) + { + // already done + } + + // Record this environment as checked so that we don't check it again on further restarts / other + // subsystem instances + addToCache(PRINCIPAL_CHECK, env); + } + } + } /** * Check if it required to use custom SSL socket factory with custom trustStore. @@ -704,5 +754,69 @@ public class LDAPInitialDirContextFactoryImpl implements LDAPInitialDirContextFa throw new AlfrescoRuntimeException("The certificates cannot be loaded from truststore.", ce); } return ks; - } + } + + private void addToCache(String key, Map value) + { + Set> envs = ldapInitialDirContextCache.get(key); + + if (envs == null) + { + envs = Collections.synchronizedSet(new HashSet>(11)); + } + + if (!envs.contains(value)) + { + envs.add(value); + } + + if (!ldapInitialDirContextCache.contains(key)) + { + ldapInitialDirContextCache.put(key, envs); + } + } + + private void removeFromCache(String key, Map value) + { + if (ldapInitialDirContextCache != null && ldapInitialDirContextCache.contains(key)) + { + Set> envs = ldapInitialDirContextCache.get(key); + if (envs != null && envs.contains(value)) + { + envs.remove(value); + if (envs.isEmpty()) + { + ldapInitialDirContextCache.remove(key); + } + } + } + } + + private boolean isCached(String key, Map value) + { + boolean isCached = false; + + if (ldapInitialDirContextCache != null && ldapInitialDirContextCache.contains(key)) + { + Set> envs = ldapInitialDirContextCache.get(key); + if (envs != null && envs.contains(value)) + { + isCached = true; + } + } + + logger.debug("LDAP check: " + key + " / isCached: " + (isCached ? "yes" : "no")); + + return isCached; + } + + public void setLdapInitialDirContextCache(SimpleCache>> cache) + { + this.ldapInitialDirContextCache = cache; + } + + public void setInitialChecksEnabled(boolean initialChecksEnabled) + { + this.initialChecksEnabled = initialChecksEnabled; + } } diff --git a/repository/src/main/java/org/alfresco/repo/thumbnail/ThumbnailRegistry.java b/repository/src/main/java/org/alfresco/repo/thumbnail/ThumbnailRegistry.java index 3d2a54cf0f..3aae11a906 100644 --- a/repository/src/main/java/org/alfresco/repo/thumbnail/ThumbnailRegistry.java +++ b/repository/src/main/java/org/alfresco/repo/thumbnail/ThumbnailRegistry.java @@ -2,11 +2,11 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2019 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is + * the paid license agreement will prevail. Otherwise, the software is * provided under the following open source license terms: * * Alfresco is free software: you can redistribute it and/or modify @@ -44,7 +44,7 @@ import org.alfresco.service.cmr.thumbnail.ThumbnailException; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; -import org.alfresco.transform.client.registry.TransformServiceRegistry; +import org.alfresco.transform.registry.TransformServiceRegistry; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; diff --git a/repository/src/main/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java b/repository/src/main/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java index 5e77b943b5..4a4c4d950e 100644 --- a/repository/src/main/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java +++ b/repository/src/main/java/org/alfresco/repo/transaction/RetryingTransactionHelper.java @@ -51,6 +51,7 @@ import org.alfresco.service.license.LicenseIntegrityException; import org.alfresco.util.LockHelper.LockTryException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.ibatis.exceptions.PersistenceException; import org.apache.ibatis.exceptions.TooManyResultsException; import org.springframework.aop.MethodBeforeAdvice; import org.springframework.aop.framework.ProxyFactory; @@ -105,7 +106,8 @@ public class RetryingTransactionHelper DataIntegrityViolationException.class, LicenseIntegrityException.class, TooManyResultsException.class, // Expected one result but found multiple (bad key alert) - LockTryException.class + LockTryException.class, + PersistenceException.class }; List> retryExceptions = new ArrayList>(); diff --git a/repository/src/main/java/org/alfresco/repo/workflow/WorkflowComponent.java b/repository/src/main/java/org/alfresco/repo/workflow/WorkflowComponent.java index d2db428450..d2622035d5 100644 --- a/repository/src/main/java/org/alfresco/repo/workflow/WorkflowComponent.java +++ b/repository/src/main/java/org/alfresco/repo/workflow/WorkflowComponent.java @@ -74,6 +74,18 @@ public interface WorkflowComponent * @since 4.0 */ public WorkflowDeployment deployDefinition(InputStream workflowDefinition, String mimetype, String name); + + /** + * Deploy a Workflow Definition + * + * @param workflowDefinition the content object containing the definition + * @param mimetype (optional) the mime type of the workflow definition + * @param name (optional) a name to represent the deployment + * @param fullAccess true if category should be defined in order to consider the deployment secure + * @return workflow deployment descriptor + * @since 4.0 + */ + public WorkflowDeployment deployDefinition(InputStream workflowDefinition, String mimetype, String name, boolean fullAccess); /** * Is the specified Workflow Definition already deployed? @@ -86,7 +98,14 @@ public interface WorkflowComponent * @return true => already deployed */ public boolean isDefinitionDeployed(InputStream workflowDefinition, String mimetype); - + + /** + * Sets the deployment category if applicable to allow the workflow to have full access + * + * @param workflowDefinition the definition to check + */ + public void checkDeploymentCategory(InputStream workflowDefinition); + /** * Undeploy an exisiting Workflow Definition * diff --git a/repository/src/main/java/org/alfresco/repo/workflow/WorkflowDeployer.java b/repository/src/main/java/org/alfresco/repo/workflow/WorkflowDeployer.java index 2fd42c0356..dc4e057e6c 100644 --- a/repository/src/main/java/org/alfresco/repo/workflow/WorkflowDeployer.java +++ b/repository/src/main/java/org/alfresco/repo/workflow/WorkflowDeployer.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -46,6 +46,7 @@ import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.Path; import org.alfresco.service.cmr.repository.StoreRef; import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.workflow.FailedWorkflowDeployment; import org.alfresco.service.cmr.workflow.WorkflowAdminService; import org.alfresco.service.cmr.workflow.WorkflowDefinition; import org.alfresco.service.cmr.workflow.WorkflowDeployment; @@ -62,11 +63,14 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.extensions.surf.util.AbstractLifecycleBean; import javax.transaction.UserTransaction; +import java.io.InputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Properties; +import java.util.function.Supplier; /** * Alfresco bootstrap Process deployment. @@ -85,6 +89,7 @@ public class WorkflowDeployer extends AbstractLifecycleBean public static final String REDEPLOY = "redeploy"; public static final String CATEGORY_ALFRESCO_INTERNAL = "http://alfresco.org/workflows/internal"; + public static final String CATEGORY_FULL_ACCESS = "http://alfresco.org/workflows/fullAccess"; // Dependencies private TransactionService transactionService; @@ -306,13 +311,17 @@ public class WorkflowDeployer extends AbstractLifecycleBean if (!redeploy && workflowService.isDefinitionDeployed(engineId, workflowResource.getInputStream(), mimetype)) { if (logger.isDebugEnabled()) - logger.debug("Workflow deployer: Definition '" + location + "' already deployed"); + { + logger.debug("Workflow deployer: Definition '" + location + "' already deployed. Checking deploymentcategory..."); + } + workflowService.checkDeploymentCategory(engineId, workflowResource.getInputStream()); } else { - WorkflowDeployment deployment = workflowService.deployDefinition(engineId, workflowResource.getInputStream(), - mimetype, workflowResource.getFilename()); - logDeployment(location, deployment); + final InputStream workflowInputStream = workflowResource.getInputStream(); + final Optional possibleDeployment = tryToDeploy(() -> + workflowService.deployDefinition(engineId, workflowInputStream, mimetype, workflowResource.getFilename(), true)); + possibleDeployment.ifPresent(deployment -> logDeployment(location, deployment)); } } else @@ -399,10 +408,9 @@ public class WorkflowDeployer extends AbstractLifecycleBean else { // deploy / re-deploy - WorkflowDeployment deployment = workflowService.deployDefinition(nodeRef); - logDeployment(nodeRef, deployment); - if (deployment != null) + tryToDeploy(() -> workflowService.deployDefinition(nodeRef)).ifPresent(deployment -> { + logDeployment(nodeRef, deployment); WorkflowDefinition def = deployment.getDefinition(); // Update the meta data for the model @@ -421,7 +429,7 @@ public class WorkflowDeployer extends AbstractLifecycleBean } nodeService.setProperties(nodeRef, props); - } + }); } } else @@ -438,6 +446,20 @@ public class WorkflowDeployer extends AbstractLifecycleBean } } + private Optional tryToDeploy(Supplier workflowDeployment) + { + final WorkflowDeployment deployment = workflowDeployment.get(); + final Optional possibleFailure = FailedWorkflowDeployment.getFailure(deployment); + + if (possibleFailure.isEmpty()) + { + return Optional.ofNullable(deployment); + } + + logger.warn("Failed to deploy a workflow. " + possibleFailure.get()); + return Optional.empty(); + } + /** * Validate that the workflow definition node is a child of the correct * workflow location node, e.g. "/Company Home/Data Dictionary/Workflows" diff --git a/repository/src/main/java/org/alfresco/repo/workflow/WorkflowInterpreter.java b/repository/src/main/java/org/alfresco/repo/workflow/WorkflowInterpreter.java index 40768cf9bc..a03fe39f4f 100644 --- a/repository/src/main/java/org/alfresco/repo/workflow/WorkflowInterpreter.java +++ b/repository/src/main/java/org/alfresco/repo/workflow/WorkflowInterpreter.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -34,6 +34,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import org.alfresco.model.ContentModel; import org.alfresco.repo.admin.BaseInterpreter; @@ -54,6 +55,7 @@ import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.service.cmr.security.PersonService; import org.alfresco.service.cmr.security.PersonService.PersonInfo; +import org.alfresco.service.cmr.workflow.FailedWorkflowDeployment; import org.alfresco.service.cmr.workflow.WorkflowDefinition; import org.alfresco.service.cmr.workflow.WorkflowDeployment; import org.alfresco.service.cmr.workflow.WorkflowException; @@ -726,10 +728,18 @@ public class WorkflowInterpreter extends BaseInterpreter { out.println(problem); } - out.println("deployed definition id: " + def.getId() + " , name: " + def.getName() + " , title: " + def.getTitle() + " , version: " + def.getVersion()); - currentDeployEngine = command[1]; - currentDeployResource = command[2]; - out.print(executeCommand("use definition " + def.getId())); + final Optional possibleDeploymentFailure = FailedWorkflowDeployment.getFailure(deployment); + if (possibleDeploymentFailure.isPresent()) + { + out.println("Failed to deploy the workflow definition."); + } + else + { + out.println("deployed definition id: " + def.getId() + " , name: " + def.getName() + " , title: " + def.getTitle() + " , version: " + def.getVersion()); + currentDeployEngine = command[1]; + currentDeployResource = command[2]; + out.print(executeCommand("use definition " + def.getId())); + } } else if (command[0].equals("redeploy")) diff --git a/repository/src/main/java/org/alfresco/repo/workflow/WorkflowServiceImpl.java b/repository/src/main/java/org/alfresco/repo/workflow/WorkflowServiceImpl.java index 85fbcab889..86d1116db7 100644 --- a/repository/src/main/java/org/alfresco/repo/workflow/WorkflowServiceImpl.java +++ b/repository/src/main/java/org/alfresco/repo/workflow/WorkflowServiceImpl.java @@ -232,9 +232,20 @@ public class WorkflowServiceImpl implements WorkflowService * .lang.String, java.io.InputStream, java.lang.String, java.lang.String) */ public WorkflowDeployment deployDefinition(String engineId, InputStream workflowDefinition, String mimetype, String name) + { + return deployDefinition(engineId, workflowDefinition, mimetype, name, false); + } + + /* + * (non-Javadoc) + * @see + * org.alfresco.service.cmr.workflow.WorkflowService#deployDefinition(java + * .lang.String, java.io.InputStream, java.lang.String, java.lang.String, boolean) + */ + public WorkflowDeployment deployDefinition(String engineId, InputStream workflowDefinition, String mimetype, String name, boolean fullAccess) { WorkflowComponent component = getWorkflowComponent(engineId); - WorkflowDeployment deployment = component.deployDefinition(workflowDefinition, mimetype, name); + WorkflowDeployment deployment = component.deployDefinition(workflowDefinition, mimetype, name, fullAccess); if (logger.isDebugEnabled() && deployment.getProblems().length > 0) { @@ -277,6 +288,18 @@ public class WorkflowServiceImpl implements WorkflowService return component.isDefinitionDeployed(workflowDefinition, mimetype); } + /* + * (non-Javadoc) + * @see + * org.alfresco.service.cmr.workflow.WorkflowService#checkDeploymentCategory + * (java.lang.String, java.io.InputStream) + */ + public void checkDeploymentCategory(String engineId, InputStream workflowDefinition) + { + WorkflowComponent component = getWorkflowComponent(engineId); + component.checkDeploymentCategory(workflowDefinition); + } + /* * (non-Javadoc) * @see diff --git a/repository/src/main/java/org/alfresco/repo/workflow/activiti/ActivitiWorkflowEngine.java b/repository/src/main/java/org/alfresco/repo/workflow/activiti/ActivitiWorkflowEngine.java index 106b5b679c..9166a5de5e 100644 --- a/repository/src/main/java/org/alfresco/repo/workflow/activiti/ActivitiWorkflowEngine.java +++ b/repository/src/main/java/org/alfresco/repo/workflow/activiti/ActivitiWorkflowEngine.java @@ -1,28 +1,28 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2016 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.repo.workflow.activiti; @@ -242,45 +242,45 @@ public class ActivitiWorkflowEngine extends BPMEngine implements WorkflowEngine /** * {@inheritDoc} */ - public WorkflowInstance cancelWorkflow(String workflowId) - { - String localId = createLocalId(workflowId); - try - { - ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(localId).singleResult(); - if (processInstance == null) - { - throw new WorkflowException(messageService.getMessage(ERR_CANCEL_UNEXISTING_WORKFLOW)); - } - - // TODO: Cancel VS delete? - // Delete the process instance - runtimeService.deleteProcessInstance(processInstance.getId(), WorkflowConstants.PROP_CANCELLED); - - // Convert historic process instance - HistoricProcessInstance deletedInstance = historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstance.getId()) - .singleResult(); - WorkflowInstance result = typeConverter.convert(deletedInstance); - - // Delete the historic process instance - // MNT-15498 - if (!activitiUtil.isRetentionHistoricProcessInstanceEnabled()) - { - historyService.deleteHistoricProcessInstance(deletedInstance.getId()); - } - - return result; - } - catch (ActivitiException ae) - { - String msg = messageService.getMessage(ERR_CANCEL_WORKFLOW); - if (logger.isDebugEnabled()) - { - logger.debug(msg, ae); - } - throw new WorkflowException(msg, ae); - } - } + public WorkflowInstance cancelWorkflow(String workflowId) + { + String localId = createLocalId(workflowId); + try + { + ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(localId).singleResult(); + if (processInstance == null) + { + throw new WorkflowException(messageService.getMessage(ERR_CANCEL_UNEXISTING_WORKFLOW)); + } + + // TODO: Cancel VS delete? + // Delete the process instance + runtimeService.deleteProcessInstance(processInstance.getId(), WorkflowConstants.PROP_CANCELLED); + + // Convert historic process instance + HistoricProcessInstance deletedInstance = historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstance.getId()) + .singleResult(); + WorkflowInstance result = typeConverter.convert(deletedInstance); + + // Delete the historic process instance + // MNT-15498 + if (!activitiUtil.isRetentionHistoricProcessInstanceEnabled()) + { + historyService.deleteHistoricProcessInstance(deletedInstance.getId()); + } + + return result; + } + catch (ActivitiException ae) + { + String msg = messageService.getMessage(ERR_CANCEL_WORKFLOW); + if (logger.isDebugEnabled()) + { + logger.debug(msg, ae); + } + throw new WorkflowException(msg, ae); + } + } /** * {@inheritDoc} @@ -336,6 +336,14 @@ public class ActivitiWorkflowEngine extends BPMEngine implements WorkflowEngine * {@inheritDoc} */ public WorkflowDeployment deployDefinition(InputStream workflowDefinition, String mimetype, String name) + { + return deployDefinition(workflowDefinition, mimetype, name, false); + } + + /** + * {@inheritDoc} + */ + public WorkflowDeployment deployDefinition(InputStream workflowDefinition, String mimetype, String name, boolean fullAccess) { try { @@ -363,6 +371,10 @@ public class ActivitiWorkflowEngine extends BPMEngine implements WorkflowEngine { repoService.setDeploymentCategory(deployment.getId(), WorkflowDeployer.CATEGORY_ALFRESCO_INTERNAL); } + else if (fullAccess) + { + repoService.setDeploymentCategory(deployment.getId(), WorkflowDeployer.CATEGORY_FULL_ACCESS); + } } // No problems can be added to the WorkflowDeployment, warnings are @@ -1005,6 +1017,46 @@ public class ActivitiWorkflowEngine extends BPMEngine implements WorkflowEngine } } + /** + * {@inheritDoc} + */ + public void checkDeploymentCategory(InputStream workflowDefinition) + { + try + { + String key = getProcessKey(workflowDefinition); + ProcessDefinition pd = activitiUtil.getProcessDefinitionByKey(key); + String deploymentId = pd.getDeploymentId(); + + List definitionList = repoService.createProcessDefinitionQuery().deploymentId(deploymentId).list(); + if (definitionList != null && definitionList.size() > 0) + { + boolean internalCategory = true; + for (ProcessDefinition processDefinition : definitionList) + { + if (WorkflowDeployer.CATEGORY_ALFRESCO_INTERNAL.equals(processDefinition.getCategory()) == false) + { + internalCategory = false; + break; + } + } + + if (!internalCategory) + { + repoService.setDeploymentCategory(deploymentId, WorkflowDeployer.CATEGORY_FULL_ACCESS); + } + } + + } + catch (Exception e) + { + if (logger.isDebugEnabled()) + { + logger.debug("Category was not set: " + e.getMessage(), e); + } + } + } + private String getProcessKey(InputStream workflowDefinition) throws Exception { try diff --git a/repository/src/main/java/org/alfresco/repo/workflow/activiti/ActivitiWorkflowManagerFactory.java b/repository/src/main/java/org/alfresco/repo/workflow/activiti/ActivitiWorkflowManagerFactory.java index ca888b9846..ca59302b18 100644 --- a/repository/src/main/java/org/alfresco/repo/workflow/activiti/ActivitiWorkflowManagerFactory.java +++ b/repository/src/main/java/org/alfresco/repo/workflow/activiti/ActivitiWorkflowManagerFactory.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -105,7 +105,7 @@ public class ActivitiWorkflowManagerFactory implements FactoryBean. - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.service.cmr.action; @@ -63,4 +63,12 @@ public interface ParameterConstraint * The implementers are expected to return allowed values in the insertion order. */ Map getAllowableValues(); + + /** + * Returns possible constraint values. + * By default returns getAllowableValues() to be backwards compatible. + */ + default Map getValues() { + return getAllowableValues(); + } } diff --git a/repository/src/main/java/org/alfresco/service/cmr/repository/ScriptProcessor.java b/repository/src/main/java/org/alfresco/service/cmr/repository/ScriptProcessor.java index d7475e9b98..bbd9d72d7c 100644 --- a/repository/src/main/java/org/alfresco/service/cmr/repository/ScriptProcessor.java +++ b/repository/src/main/java/org/alfresco/service/cmr/repository/ScriptProcessor.java @@ -70,10 +70,21 @@ public interface ScriptProcessor extends Processor * * @param script the script string * @param model the context model - * @return Obejct the result of the script + * @return Object the result of the script */ public Object executeString(String script, Map model); - + + /** + * Execute script string + * + * @param script the script string + * @param model the context model + * @param secure the flag that indicates if string is considered secure to be executed, i.e., it will have + * access to the full execution context instead of being executed in a sandbox context + * @return Object the result of the script + */ + public Object executeString(String script, Map model, boolean secure); + /** * Reset the processor - such as clearing any internal caches etc. */ diff --git a/repository/src/main/java/org/alfresco/service/cmr/repository/ScriptService.java b/repository/src/main/java/org/alfresco/service/cmr/repository/ScriptService.java index fc2b807432..e231c8433e 100644 --- a/repository/src/main/java/org/alfresco/service/cmr/repository/ScriptService.java +++ b/repository/src/main/java/org/alfresco/service/cmr/repository/ScriptService.java @@ -160,7 +160,22 @@ public interface ScriptService @Auditable(parameters = {"script", "model"}) public Object executeScriptString(String script, Map model) throws ScriptException; - + + /** + * Process a script against the supplied data model. Uses the default script engine. + * + * @param script Script content as a String. + * @param model Object model to process script against + * @param secure A flag indicating if string script is considered secure (e.g., if it comes from the classpath) + * If true it will have access to the full execution context, if false the script will be executed in a sandbox context (more restricted) + * @return output of the script (may be null or any valid wrapped JavaScript object) + * + * @throws ScriptException + */ + @Auditable(parameters = {"script", "model", "secure"}) + public Object executeScriptString(String script, Map model, boolean secure) + throws ScriptException; + /** * Process a script against the supplied data model. * @@ -175,7 +190,23 @@ public interface ScriptService @Auditable(parameters = {"engine", "script", "model"}) public Object executeScriptString(String engine, String script, Map model) throws ScriptException; - + + /** + * Process a script against the supplied data model. + * + * @param engine the script engine to use + * @param script Script content as a String. + * @param model Object model to process script against + * @param secure A flag indicating if string script is considered secure + * + * @return output of the script (may be null or any valid wrapped JavaScript object) + * + * @throws ScriptException + */ + @Auditable(parameters = {"engine", "script", "model", "secure"}) + public Object executeScriptString(String engine, String script, Map model, boolean secure) + throws ScriptException; + /** * Registers a script processor with the script service * diff --git a/repository/src/main/java/org/alfresco/service/cmr/rule/RuleService.java b/repository/src/main/java/org/alfresco/service/cmr/rule/RuleService.java index 36e09cdd74..8b84ebc53d 100644 --- a/repository/src/main/java/org/alfresco/service/cmr/rule/RuleService.java +++ b/repository/src/main/java/org/alfresco/service/cmr/rule/RuleService.java @@ -215,7 +215,41 @@ public interface RuleService */ @Auditable(parameters = {"nodeRef"}) public int countRules(NodeRef nodeRef); - + + /** + * Traverse the folder hierarchy find all the folder nodes that could supply rules by inheritance. + * + * @param nodeRef The starting node ref. + * @return A list of node refs, starting with the first parent of the first parent of ... and ending with the object generated by the + * given node ref. + */ + @Auditable (parameters = { "nodeRef" }) + @Experimental + List getNodesSupplyingRuleSets(NodeRef nodeRef); + + /** + * Get a list of folders inheriting the specified rule set. + * + * @param ruleSet The rule set node. + * @param maxFoldersToReturn A limit on the number of folders to return (since otherwise this could traverse a very large proportion of + * the repository). + * @return The list of the inheriting folders. + */ + @Auditable (parameters = { "ruleSet", "maxFoldersToReturn" }) + @Experimental + List getFoldersInheritingRuleSet(NodeRef ruleSet, int maxFoldersToReturn); + + /** + * Get a list of folders linking to the specified rule set. + * + * @param ruleSet The rule set node. + * @param maxFoldersToReturn A limit on the number of folders to return. + * @return The list linking folders. + */ + @Auditable (parameters = { "ruleSet" }) + @Experimental + List getFoldersLinkingToRuleSet(NodeRef ruleSet, int maxFoldersToReturn); + /** * Get the rule given its node reference * @@ -235,8 +269,8 @@ public interface RuleService * @param rule Rule */ @Auditable(parameters = {"nodeRef", "rule"}) - public void saveRule(NodeRef nodeRef, Rule rule); - + public Rule saveRule(NodeRef nodeRef, Rule rule); + /** * * @param nodeRef NodeRef @@ -300,7 +334,17 @@ public interface RuleService */ @Auditable(parameters = {"action"}) public NodeRef getOwningNodeRef(Action action); - + + /** + * Returns the owning node reference for a rule. + * + * @param ruleSet The rule set node. + * @return the owning node reference + */ + @Auditable (parameters = { "ruleSet" }) + @Experimental + NodeRef getOwningNodeRef(NodeRef ruleSet); + /** * Indicates whether the passed rule node reference is linked to another * rule node. @@ -342,7 +386,7 @@ public interface RuleService NodeRef getRuleSetNode(final NodeRef folderNodeRef); /** - * Check if rule set's associated parent matches folder node. + * Check if rule set is associated (owned/linked/inherited) with the given folder node. * * @param ruleSetNodeRef - node reference of a rule set * @param folderNodeRef - node reference of a folder @@ -362,4 +406,14 @@ public interface RuleService @Auditable(parameters = {"ruleNodeRef", "ruleSetNodeRef"}) @Experimental boolean isRuleAssociatedWithRuleSet(final NodeRef ruleNodeRef, final NodeRef ruleSetNodeRef); + + /** + * Check if other folders are linked to rule set. + * + * @param ruleSetNodeRef - node reference of a rule set + * @return true if others folders are linked to rule set + */ + @Auditable(parameters = {"ruleSetNodeRef"}) + @Experimental + boolean isRuleSetShared(final NodeRef ruleSetNodeRef); } diff --git a/repository/src/main/java/org/alfresco/service/cmr/view/ExporterContext.java b/repository/src/main/java/org/alfresco/service/cmr/view/ExporterContext.java index b3162b0519..d6b5dc1c93 100644 --- a/repository/src/main/java/org/alfresco/service/cmr/view/ExporterContext.java +++ b/repository/src/main/java/org/alfresco/service/cmr/view/ExporterContext.java @@ -1,31 +1,32 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.service.cmr.view; import java.util.Date; +import java.util.Map; import org.alfresco.service.cmr.repository.NodeRef; @@ -73,6 +74,8 @@ public interface ExporterContext * @return NodeRef[] */ public NodeRef[] getExportList(); + + public Map getExportMap(); /** * Gets list of parents for exporting nodes diff --git a/repository/src/main/java/org/alfresco/service/cmr/view/ExporterService.java b/repository/src/main/java/org/alfresco/service/cmr/view/ExporterService.java index 934653bd51..47e5fe3320 100644 --- a/repository/src/main/java/org/alfresco/service/cmr/view/ExporterService.java +++ b/repository/src/main/java/org/alfresco/service/cmr/view/ExporterService.java @@ -1,28 +1,28 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.service.cmr.view; import java.io.OutputStream; @@ -75,5 +75,6 @@ public interface ExporterService */ @Auditable(parameters = {"exporter", "parameters", "progress"}) public void exportView(Exporter exporter, ExporterCrawlerParameters parameters, Exporter progress); - + + public void setExportChunkSize(String exportChunkSize); } diff --git a/repository/src/main/java/org/alfresco/service/cmr/workflow/FailedWorkflowDeployment.java b/repository/src/main/java/org/alfresco/service/cmr/workflow/FailedWorkflowDeployment.java new file mode 100644 index 0000000000..503d89bae9 --- /dev/null +++ b/repository/src/main/java/org/alfresco/service/cmr/workflow/FailedWorkflowDeployment.java @@ -0,0 +1,71 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.service.cmr.workflow; + +import java.util.Optional; + +/** + * The goal of this class is to provide a support for workflow deployment failure. Since {@link WorkflowDeployment} is + * part of the public API we don't want to change it. + */ +public final class FailedWorkflowDeployment +{ + private FailedWorkflowDeployment() + { + //no instantiation + } + + public static WorkflowDeployment deploymentForbidden(String workflowName, String reason) + { + return new DeploymentFailure(workflowName, reason); + } + + public static Optional getFailure(WorkflowDeployment workflowDeployment) + { + if (!(workflowDeployment instanceof DeploymentFailure)) + { + return Optional.empty(); + } + + return Optional.of(workflowDeployment.getProblems()[0]); + } + + private static class DeploymentFailure extends WorkflowDeployment + { + private static final String UNDEFINED = "undefined"; + + private DeploymentFailure(String workflowName, String problemDescription) + { + super(failedDefinition(workflowName), problemDescription); + } + + private static WorkflowDefinition failedDefinition(String workflowName) + { + final String definitionName = workflowName == null ? UNDEFINED : workflowName; + return new WorkflowDefinition(UNDEFINED, definitionName, UNDEFINED, UNDEFINED, UNDEFINED, null); + } + } +} diff --git a/repository/src/main/java/org/alfresco/service/cmr/workflow/WorkflowService.java b/repository/src/main/java/org/alfresco/service/cmr/workflow/WorkflowService.java index ffb27965e4..891f8cb21a 100644 --- a/repository/src/main/java/org/alfresco/service/cmr/workflow/WorkflowService.java +++ b/repository/src/main/java/org/alfresco/service/cmr/workflow/WorkflowService.java @@ -77,7 +77,24 @@ public interface WorkflowService parameters = {"engineId", "workflowDefinition", "mimetype", "name"}, recordable = {true, false, true, true}) public WorkflowDeployment deployDefinition(String engineId, InputStream workflowDefinition, String mimetype, String name); - + + /** + * Deploy a Workflow Definition to the Alfresco Repository + * + * @param engineId the bpm engine id + * @param workflowDefinition the workflow definition + * @param mimetype the mimetype of the workflow definition + * @param name a name representing the deployment + * @parm fullAccess true if workflow should be considered secure (e.g., if it is deployed in classpath) and should have full access to the execution context, + * false if it should be executed in a sandbox context (more restricted) + * @return workflow deployment descriptor + * @since 4.0 + */ + @Auditable( + parameters = {"engineId", "workflowDefinition", "mimetype", "name", "fullAccess"}, + recordable = {true, false, true, true, true}) + public WorkflowDeployment deployDefinition(String engineId, InputStream workflowDefinition, String mimetype, String name, boolean fullAccess); + /** * Deploy a Workflow Definition to the Alfresco Repository * @@ -117,6 +134,17 @@ public interface WorkflowService parameters = {"engineId", "workflowDefinition", "mimetype"}, recordable = {true, false, true}) public boolean isDefinitionDeployed(String engineId, InputStream workflowDefinition, String mimetype); + + /** + * Checks if the deployment for supplied workflow definition has the proper category + * + * @param engineId the bpm engine id + * @param workflowDefinition the definition to check + */ + @Auditable( + parameters = {"engineId", "workflowDefinition"}, + recordable = {true, false}) + public void checkDeploymentCategory(String engineId, InputStream workflowDefinition); /** * Undeploy an exisiting Workflow Definition diff --git a/repository/src/main/java/org/alfresco/service/license/LicenseDescriptor.java b/repository/src/main/java/org/alfresco/service/license/LicenseDescriptor.java index f8bd4e4d1b..e60da2e00a 100644 --- a/repository/src/main/java/org/alfresco/service/license/LicenseDescriptor.java +++ b/repository/src/main/java/org/alfresco/service/license/LicenseDescriptor.java @@ -139,7 +139,17 @@ public interface LicenseDescriptor * @return true if the license allows cryptodoc */ boolean isCryptodocEnabled(); - + + /** + * Does this license allow custom embedded workflows? + * + * @return true if the license allows custom embedded workflows + */ + default boolean isCustomEmbeddedWorkflowEnabled() + { + return false; + } + /** * ATS Transformation Server Expiry Date * @return the ATS Transformation Server Expiry Date or null diff --git a/repository/src/main/java/org/alfresco/transform/client/registry/CombinedConfig.java b/repository/src/main/java/org/alfresco/transform/registry/CombinedConfig.java similarity index 97% rename from repository/src/main/java/org/alfresco/transform/client/registry/CombinedConfig.java rename to repository/src/main/java/org/alfresco/transform/registry/CombinedConfig.java index 12372baa49..dfcd8fb466 100644 --- a/repository/src/main/java/org/alfresco/transform/client/registry/CombinedConfig.java +++ b/repository/src/main/java/org/alfresco/transform/registry/CombinedConfig.java @@ -23,15 +23,15 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.transform.client.registry; +package org.alfresco.transform.registry; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.content.transform.LocalPassThroughTransform; import org.alfresco.service.cmr.repository.MimetypeService; -import org.alfresco.transform.client.model.config.TransformConfig; -import org.alfresco.transform.client.model.config.Transformer; +import org.alfresco.transform.config.TransformConfig; +import org.alfresco.transform.config.Transformer; import org.alfresco.util.ConfigFileFinder; import org.apache.commons.logging.Log; import org.apache.http.HttpEntity; @@ -47,7 +47,7 @@ import java.io.StringReader; import java.util.Collections; import java.util.List; -import static org.alfresco.transform.client.util.RequestParamMap.ENDPOINT_TRANSFORM_CONFIG_LATEST; +import static org.alfresco.transform.common.RequestParamMap.ENDPOINT_TRANSFORM_CONFIG_LATEST; /** * This class reads multiple T-Engine config and local files and registers as if they were all diff --git a/repository/src/main/java/org/alfresco/transform/client/registry/TransformServiceRegistryImpl.java b/repository/src/main/java/org/alfresco/transform/registry/TransformServiceRegistryImpl.java similarity index 95% rename from repository/src/main/java/org/alfresco/transform/client/registry/TransformServiceRegistryImpl.java rename to repository/src/main/java/org/alfresco/transform/registry/TransformServiceRegistryImpl.java index 72cca58777..55e1079683 100644 --- a/repository/src/main/java/org/alfresco/transform/client/registry/TransformServiceRegistryImpl.java +++ b/repository/src/main/java/org/alfresco/transform/registry/TransformServiceRegistryImpl.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2020 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -23,9 +23,12 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.transform.client.registry; +package org.alfresco.transform.registry; import com.fasterxml.jackson.databind.ObjectMapper; +import org.alfresco.transform.registry.AbstractTransformRegistry; +import org.alfresco.transform.registry.SupportedTransform; +import org.alfresco.transform.registry.TransformCache; import org.alfresco.util.ConfigScheduler; import org.alfresco.util.PropertyCheck; import org.alfresco.util.ShutdownIndicator; @@ -38,7 +41,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import static org.alfresco.transform.client.registry.TransformRegistryHelper.retrieveTransformListBySize; +import static org.alfresco.transform.registry.TransformRegistryHelper.retrieveTransformListBySize; /** * Used by clients to work out if a transformation is supported by the Transform Service. diff --git a/repository/src/main/resources/alfresco/bootstrap-context.xml b/repository/src/main/resources/alfresco/bootstrap-context.xml index 31d9da6957..40868b38c4 100644 --- a/repository/src/main/resources/alfresco/bootstrap-context.xml +++ b/repository/src/main/resources/alfresco/bootstrap-context.xml @@ -135,6 +135,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -255,26 +275,6 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/repository/src/main/resources/alfresco/cache-context.xml b/repository/src/main/resources/alfresco/cache-context.xml index a04dbbbc72..6cb3c7e1dc 100644 --- a/repository/src/main/resources/alfresco/cache-context.xml +++ b/repository/src/main/resources/alfresco/cache-context.xml @@ -506,4 +506,12 @@ + + + + + + + + diff --git a/repository/src/main/resources/alfresco/caches.properties b/repository/src/main/resources/alfresco/caches.properties index fb58b12919..3cb0fc487e 100644 --- a/repository/src/main/resources/alfresco/caches.properties +++ b/repository/src/main/resources/alfresco/caches.properties @@ -699,3 +699,15 @@ cache.queryAcceleratorCache.backup-count=1 cache.queryAcceleratorCache.eviction-policy=NONE cache.queryAcceleratorCache.merge-policy=com.hazelcast.map.merge.LatestUpdateMapMergePolicy cache.queryAcceleratorCache.readBackupData=false + +# +# LDAP initial dir context checks cluster cache +# +cache.ldapInitialDirContextCache.maxItems=100 +cache.ldapInitialDirContextCache.timeToLiveSeconds=0 +cache.ldapInitialDirContextCache.maxIdleSeconds=0 +cache.ldapInitialDirContextCache.cluster.type=fully-distributed +cache.ldapInitialDirContextCache.backup-count=1 +cache.ldapInitialDirContextCache.eviction-policy=NONE +cache.ldapInitialDirContextCache.merge-policy=com.hazelcast.map.merge.LatestUpdateMapMergePolicy +cache.ldapInitialDirContextCache.readBackupData=false \ No newline at end of file diff --git a/repository/src/main/resources/alfresco/dbscripts/utility/org.alfresco.repo.domain.dialect.MySQLInnoDBDialect/CleanAlfPropTablesV3.sql b/repository/src/main/resources/alfresco/dbscripts/utility/org.alfresco.repo.domain.dialect.MySQLInnoDBDialect/CleanAlfPropTablesV3.sql new file mode 100644 index 0000000000..c95d3f0a45 --- /dev/null +++ b/repository/src/main/resources/alfresco/dbscripts/utility/org.alfresco.repo.domain.dialect.MySQLInnoDBDialect/CleanAlfPropTablesV3.sql @@ -0,0 +1,9 @@ +--DELETE_NOT_EXISTS_V3 alf_prop_root.id,alf_audit_app.disabled_paths_id,alf_audit_entry.audit_values_id,alf_prop_unique_ctx.prop1_id system.delete_not_exists.batchsize + +--DELETE_NOT_EXISTS_V3 alf_prop_value.id,alf_audit_app.app_name_id,alf_audit_entry.audit_user_id,alf_prop_link.key_prop_id,alf_prop_link.value_prop_id,alf_prop_unique_ctx.value1_prop_id,alf_prop_unique_ctx.value2_prop_id,alf_prop_unique_ctx.value3_prop_id system.delete_not_exists.batchsize + +--DELETE_NOT_EXISTS_V3 alf_prop_string_value.id,alf_prop_value.long_value."persisted_type in (3, 5, 6)",alf_audit_app.app_name_id,alf_audit_entry.audit_user_id,alf_prop_link.key_prop_id,alf_prop_link.value_prop_id,alf_prop_unique_ctx.value1_prop_id,alf_prop_unique_ctx.value2_prop_id,alf_prop_unique_ctx.value3_prop_id system.delete_not_exists.batchsize + +--DELETE_NOT_EXISTS_V3 alf_prop_serializable_value.id,alf_prop_value.long_value.persisted_type=4,alf_audit_app.app_name_id,alf_audit_entry.audit_user_id,alf_prop_link.key_prop_id,alf_prop_link.value_prop_id,alf_prop_unique_ctx.value1_prop_id,alf_prop_unique_ctx.value2_prop_id,alf_prop_unique_ctx.value3_prop_id system.delete_not_exists.batchsize + +--DELETE_NOT_EXISTS_V3 alf_prop_double_value.id,alf_prop_value.long_value.persisted_type=2,alf_audit_app.app_name_id,alf_audit_entry.audit_user_id,alf_prop_link.key_prop_id,alf_prop_link.value_prop_id,alf_prop_unique_ctx.value1_prop_id,alf_prop_unique_ctx.value2_prop_id,alf_prop_unique_ctx.value3_prop_id system.delete_not_exists.batchsize diff --git a/repository/src/main/resources/alfresco/dbscripts/utility/org.alfresco.repo.domain.dialect.PostgreSQLDialect/CleanAlfPropTablesV3.sql b/repository/src/main/resources/alfresco/dbscripts/utility/org.alfresco.repo.domain.dialect.PostgreSQLDialect/CleanAlfPropTablesV3.sql new file mode 100644 index 0000000000..c95d3f0a45 --- /dev/null +++ b/repository/src/main/resources/alfresco/dbscripts/utility/org.alfresco.repo.domain.dialect.PostgreSQLDialect/CleanAlfPropTablesV3.sql @@ -0,0 +1,9 @@ +--DELETE_NOT_EXISTS_V3 alf_prop_root.id,alf_audit_app.disabled_paths_id,alf_audit_entry.audit_values_id,alf_prop_unique_ctx.prop1_id system.delete_not_exists.batchsize + +--DELETE_NOT_EXISTS_V3 alf_prop_value.id,alf_audit_app.app_name_id,alf_audit_entry.audit_user_id,alf_prop_link.key_prop_id,alf_prop_link.value_prop_id,alf_prop_unique_ctx.value1_prop_id,alf_prop_unique_ctx.value2_prop_id,alf_prop_unique_ctx.value3_prop_id system.delete_not_exists.batchsize + +--DELETE_NOT_EXISTS_V3 alf_prop_string_value.id,alf_prop_value.long_value."persisted_type in (3, 5, 6)",alf_audit_app.app_name_id,alf_audit_entry.audit_user_id,alf_prop_link.key_prop_id,alf_prop_link.value_prop_id,alf_prop_unique_ctx.value1_prop_id,alf_prop_unique_ctx.value2_prop_id,alf_prop_unique_ctx.value3_prop_id system.delete_not_exists.batchsize + +--DELETE_NOT_EXISTS_V3 alf_prop_serializable_value.id,alf_prop_value.long_value.persisted_type=4,alf_audit_app.app_name_id,alf_audit_entry.audit_user_id,alf_prop_link.key_prop_id,alf_prop_link.value_prop_id,alf_prop_unique_ctx.value1_prop_id,alf_prop_unique_ctx.value2_prop_id,alf_prop_unique_ctx.value3_prop_id system.delete_not_exists.batchsize + +--DELETE_NOT_EXISTS_V3 alf_prop_double_value.id,alf_prop_value.long_value.persisted_type=2,alf_audit_app.app_name_id,alf_audit_entry.audit_user_id,alf_prop_link.key_prop_id,alf_prop_link.value_prop_id,alf_prop_unique_ctx.value1_prop_id,alf_prop_unique_ctx.value2_prop_id,alf_prop_unique_ctx.value3_prop_id system.delete_not_exists.batchsize diff --git a/repository/src/main/resources/alfresco/import-export-context.xml b/repository/src/main/resources/alfresco/import-export-context.xml index d986471533..5e66eb6ec4 100644 --- a/repository/src/main/resources/alfresco/import-export-context.xml +++ b/repository/src/main/resources/alfresco/import-export-context.xml @@ -130,6 +130,9 @@ + + ${rm.export.chunk.size} + diff --git a/repository/src/main/resources/alfresco/messages/module-messages.properties b/repository/src/main/resources/alfresco/messages/module-messages.properties index 9040f17188..59f7f7bc71 100644 --- a/repository/src/main/resources/alfresco/messages/module-messages.properties +++ b/repository/src/main/resources/alfresco/messages/module-messages.properties @@ -18,3 +18,4 @@ module.err.component_already_registered=A component named ''{0}'' has already be module.err.unable_to_open_module_properties=The module properties file ''{0}'' could not be read. module.err.component_in_missing_module=The component ''{0}'' belongs to a non-existent module ''{1}''. module.err.orphaned_components={0} module components were not considered for execution. +module.err.deprecated_modules=The following deprecated modules are installed: {0}. Please remove these AMPs in order to continue. \ No newline at end of file diff --git a/repository/src/main/resources/alfresco/module-context.xml b/repository/src/main/resources/alfresco/module-context.xml index 87999b274e..218c37f213 100644 --- a/repository/src/main/resources/alfresco/module-context.xml +++ b/repository/src/main/resources/alfresco/module-context.xml @@ -24,8 +24,17 @@ + + + + + alfresco-saml-repo + + + + - + diff --git a/repository/src/main/resources/alfresco/repository.properties b/repository/src/main/resources/alfresco/repository.properties index 7954cff846..797d9b6858 100644 --- a/repository/src/main/resources/alfresco/repository.properties +++ b/repository/src/main/resources/alfresco/repository.properties @@ -3,7 +3,7 @@ repository.name=Main Repository # Schema number -version.schema=17000 +version.schema=18000 # Directory configuration @@ -53,6 +53,7 @@ system.webdav.servlet.enabled=true system.webdav.url.path.prefix= system.webdav.storeName=${protocols.storeName} system.webdav.rootPath=${protocols.rootPath} +system.webdav.allowInsecurePOSTMethod=false # File name patterns that trigger rename shuffle detection # pattern is used by move - tested against full path after it has been lower cased. system.webdav.renameShufflePattern=(.*/\\..*)|(.*[a-f0-9]{8}+$)|(.*\\.tmp$)|(.*atmp[0-9]+$)|(.*\\.wbk$)|(.*\\.bak$)|(.*\\~$)|(.*backup.*\\.do[ct]{1}[x]?[m]?$)|(.*\\.sb\\-\\w{8}\\-\\w{6}$) @@ -1246,6 +1247,12 @@ system.delete_not_exists.read_only=false system.delete_not_exists.timeout_seconds=-1 system.prop_table_cleaner.algorithm=V2 +#Aditional options for algorithm V3 +#After how many rows processed do we pause the job to allow the DB to recover +system.delete_not_exists.pauseAndRecoverBatchSize=500000 +#Duration of the pause in milliseconds (default 10s) +system.delete_not_exists.pauseAndRecoverTime=10000 + # --Node cleanup batch - default settings system.node_cleanup.delete_batchSize=1000 system.node_table_cleaner.algorithm=V1 diff --git a/repository/src/main/resources/alfresco/subsystems/Authentication/common-ldap-context.xml b/repository/src/main/resources/alfresco/subsystems/Authentication/common-ldap-context.xml index efc67f4f3a..8c50fce463 100644 --- a/repository/src/main/resources/alfresco/subsystems/Authentication/common-ldap-context.xml +++ b/repository/src/main/resources/alfresco/subsystems/Authentication/common-ldap-context.xml @@ -119,7 +119,12 @@ --> - + + + + + ${ldap.authentication.initial.checks.enabled} + ${ldap.authentication.truststore.path} diff --git a/repository/src/main/resources/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties b/repository/src/main/resources/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties index 9082d53294..b6bb243252 100644 --- a/repository/src/main/resources/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties +++ b/repository/src/main/resources/alfresco/subsystems/Authentication/ldap-ad/ldap-ad-authentication.properties @@ -175,3 +175,6 @@ ldap.synchronization.userAccountStatusProperty=userAccountControl # The Account Status Interpreter bean name ldap.synchronization.userAccountStatusInterpreter=ldapadUserAccountStatusInterpreter + +# Allows to enable or disable LDAP-AD initial checks (anonymous bind, simple dn, dn and principal) +ldap.authentication.initial.checks.enabled=true \ No newline at end of file diff --git a/repository/src/main/resources/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties b/repository/src/main/resources/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties index 6b80f84694..9b3804013f 100644 --- a/repository/src/main/resources/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties +++ b/repository/src/main/resources/alfresco/subsystems/Authentication/ldap/ldap-authentication.properties @@ -192,3 +192,6 @@ ldap.synchronization.disabledAccountPropertyValueCanBeNull=true # The Account Status Interpreter bean name ldap.synchronization.userAccountStatusInterpreter=ldapUserAccountStatusInterpreter + +# Allows to enable or disable LDAP initial checks (anonymous bind, simple dn, dn and principal) +ldap.authentication.initial.checks.enabled=true \ No newline at end of file diff --git a/repository/src/test/java/org/alfresco/AllDBTestsTestSuite.java b/repository/src/test/java/org/alfresco/AllDBTestsTestSuite.java index 83355816a6..1a975a4634 100644 --- a/repository/src/test/java/org/alfresco/AllDBTestsTestSuite.java +++ b/repository/src/test/java/org/alfresco/AllDBTestsTestSuite.java @@ -88,6 +88,7 @@ import org.junit.runners.Suite; org.alfresco.repo.security.person.GetPeopleCannedQueryTest.class, org.alfresco.repo.domain.schema.script.DeleteNotExistsExecutorTest.class, + org.alfresco.repo.domain.schema.script.DeleteNotExistsV3ExecutorTest.class, org.alfresco.repo.node.cleanup.DeletedNodeBatchCleanupTest.class }) public class AllDBTestsTestSuite diff --git a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java index 54924edee0..e0642cee21 100644 --- a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java +++ b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java @@ -67,14 +67,17 @@ import org.junit.runners.Suite; org.alfresco.repo.management.subsystems.CryptodocSwitchableApplicationContextFactoryTest.class, org.alfresco.repo.module.ModuleDetailsImplTest.class, org.alfresco.repo.module.ModuleVersionNumberTest.class, + org.alfresco.repo.module.DeprecatedModulesValidatorTest.class, org.alfresco.repo.node.integrity.IntegrityEventTest.class, org.alfresco.repo.policy.MTPolicyComponentTest.class, org.alfresco.repo.policy.PolicyComponentTest.class, org.alfresco.repo.rendition.RenditionNodeManagerTest.class, org.alfresco.repo.rendition.RenditionServiceImplTest.class, org.alfresco.repo.replication.ReplicationServiceImplTest.class, + org.alfresco.repo.rule.RuleServiceImplUnitTest.class, org.alfresco.repo.service.StoreRedirectorProxyFactoryTest.class, org.alfresco.repo.site.RoleComparatorImplTest.class, + org.alfresco.repo.template.UnsafeMethodsTest.class, org.alfresco.repo.tenant.MultiTAdminServiceImplTest.class, org.alfresco.repo.thumbnail.ThumbnailServiceImplParameterTest.class, org.alfresco.repo.transfer.ContentChunkerImplTest.class, diff --git a/repository/src/test/java/org/alfresco/AppContext01TestSuite.java b/repository/src/test/java/org/alfresco/AppContext01TestSuite.java index 1533550859..277bb60627 100644 --- a/repository/src/test/java/org/alfresco/AppContext01TestSuite.java +++ b/repository/src/test/java/org/alfresco/AppContext01TestSuite.java @@ -51,6 +51,10 @@ import org.junit.runners.Suite; org.alfresco.repo.action.executer.ContentMetadataEmbedderTest.class, org.alfresco.repo.action.executer.AsynchronousExtractorTest.class, + org.alfresco.repo.action.access.ActionAccessRestrictionAbstractBaseTest.class, + org.alfresco.repo.action.access.ActionAccessRestrictionTest.class, + org.alfresco.repo.action.access.AdminActionAccessRestrictionTest.class, + org.alfresco.repo.rule.RuleLinkTest.class, org.alfresco.repo.rule.RuleServiceCoverageTest.class, org.alfresco.repo.rule.RuleServiceImplTest.class, diff --git a/repository/src/test/java/org/alfresco/AppContext06TestSuite.java b/repository/src/test/java/org/alfresco/AppContext06TestSuite.java index c11a07821d..49bede2566 100644 --- a/repository/src/test/java/org/alfresco/AppContext06TestSuite.java +++ b/repository/src/test/java/org/alfresco/AppContext06TestSuite.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2017 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -50,7 +50,7 @@ import org.junit.runners.Suite; org.alfresco.repo.event2.RepoEvent2ITSuite.class, // Requires running transformers - org.alfresco.transform.client.registry.LocalTransformServiceRegistryConfigTest.class, + org.alfresco.transform.registry.LocalTransformServiceRegistryConfigTest.class, org.alfresco.repo.rendition2.RenditionService2IntegrationTest.class, org.alfresco.repo.rendition2.LocalTransformServiceRegistryIntegrationTest.class, org.alfresco.repo.rendition2.LocalTransformClientIntegrationTest.class, diff --git a/repository/src/test/java/org/alfresco/repo/action/access/ActionAccessRestrictionAbstractBaseTest.java b/repository/src/test/java/org/alfresco/repo/action/access/ActionAccessRestrictionAbstractBaseTest.java new file mode 100644 index 0000000000..305de60868 --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/action/access/ActionAccessRestrictionAbstractBaseTest.java @@ -0,0 +1,127 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.action.access; + +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.service.cmr.action.Action; +import org.junit.Before; +import org.junit.Test; +import org.springframework.lang.NonNull; + +import java.util.Properties; + +import static org.junit.Assert.assertThrows; + +public class ActionAccessRestrictionAbstractBaseTest { + + private static final String GLOBAL_PROPERTIES_ACTION_EXPOSE_PREFIX = "org.alfresco.repo.action"; + private static final String GLOBAL_PROPERTIES_ACTION_EXPOSE_SUFFIX = ".exposed"; + private static final String CONTROLLED_CONTEXT = ActionAccessRestriction.V1_ACTION_CONTEXT; + private static final String NONCONTROLLED_CONTEXT = "random321"; + + private static final String MAIL_ACTION = "mail"; + + + private ActionAccessRestrictionAbstractBase accessRestriction; + private Properties globalProperties; + + @Before + public void setup() { + globalProperties = new Properties(); + + accessRestriction = new ActionAccessRestrictionAbstractBase() { + @Override + protected void innerVerifyAccessRestriction(Action action) { + throw new ActionAccessException("Executing verification"); + } + }; + accessRestriction.setConfigProperties(globalProperties); + } + + @Test + public void skipVerificationForNullContext() { + Action action = getActionWithContext(MAIL_ACTION, null); + accessRestriction.verifyAccessRestriction(action); + } + + @Test + public void skipVerificationForNonControlledContext() { + Action action = getActionWithContext(MAIL_ACTION, NONCONTROLLED_CONTEXT); + accessRestriction.verifyAccessRestriction(action); + } + + @Test + public void callVerificationForControlledContext() { + Action action = getActionWithContext(MAIL_ACTION, CONTROLLED_CONTEXT); + assertThrows(ActionAccessException.class, () -> accessRestriction.verifyAccessRestriction(action)); + } + + @Test + public void skipVerificationForExposedActionConfig() { + setGlobalPropertiesActionExposed(MAIL_ACTION, null, true); + Action action = getActionWithContext(MAIL_ACTION, CONTROLLED_CONTEXT); + accessRestriction.verifyAccessRestriction(action); + } + + @Test + public void skipVerificationForExposedActionContextConfig() { + setGlobalPropertiesActionExposed(MAIL_ACTION, CONTROLLED_CONTEXT, true); + Action action = getActionWithContext(MAIL_ACTION, CONTROLLED_CONTEXT); + accessRestriction.verifyAccessRestriction(action); + } + + @Test + public void callVerificationForNonExposedActionConfig() { + setGlobalPropertiesActionExposed(MAIL_ACTION, null, false); + Action action = getActionWithContext(MAIL_ACTION, CONTROLLED_CONTEXT); + assertThrows(ActionAccessException.class, () -> accessRestriction.verifyAccessRestriction(action)); + } + + @Test + public void callVerificationForNonExposedActionContextConfig() { + setGlobalPropertiesActionExposed(MAIL_ACTION, CONTROLLED_CONTEXT, false); + Action action = getActionWithContext(MAIL_ACTION, CONTROLLED_CONTEXT); + assertThrows(ActionAccessException.class, () -> accessRestriction.verifyAccessRestriction(action)); + } + + private Action getActionWithContext(String actionName, String context) { + Action action = new ActionImpl(null, "12345", actionName); + ActionAccessRestriction.setActionContext(action, context); + + return action; + } + + private void setGlobalPropertiesActionExposed(@NonNull String action, String context, boolean isExposed) { + StringBuilder property = new StringBuilder(GLOBAL_PROPERTIES_ACTION_EXPOSE_PREFIX); + property.append("." + action); + if (context != null) { + property.append("." + context); + } + property.append(GLOBAL_PROPERTIES_ACTION_EXPOSE_SUFFIX); + + globalProperties.setProperty(property.toString(), Boolean.toString(isExposed)); + } +} diff --git a/repository/src/test/java/org/alfresco/repo/action/access/ActionAccessRestrictionTest.java b/repository/src/test/java/org/alfresco/repo/action/access/ActionAccessRestrictionTest.java new file mode 100644 index 0000000000..775c7c3add --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/action/access/ActionAccessRestrictionTest.java @@ -0,0 +1,43 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.action.access; + +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.service.cmr.action.Action; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ActionAccessRestrictionTest { + + @Test + public void testSettingContext() { + Action mailAction = new ActionImpl(null, "12345", "mail"); + + ActionAccessRestriction.setActionContext(mailAction, ActionAccessRestriction.RULE_ACTION_CONTEXT); + assertEquals(ActionAccessRestriction.RULE_ACTION_CONTEXT, ActionAccessRestriction.getActionContext(mailAction)); + } +} diff --git a/repository/src/test/java/org/alfresco/repo/action/access/AdminActionAccessRestrictionTest.java b/repository/src/test/java/org/alfresco/repo/action/access/AdminActionAccessRestrictionTest.java new file mode 100644 index 0000000000..f574cb219c --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/action/access/AdminActionAccessRestrictionTest.java @@ -0,0 +1,89 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.action.access; + +import org.alfresco.repo.action.ActionImpl; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.util.BaseSpringTest; +import org.junit.Before; +import org.junit.Test; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertThrows; + +public class AdminActionAccessRestrictionTest extends BaseSpringTest { + + private static final String MAIL_ACTION = "mail"; + private static final String CONTROLLED_CONTEXT = ActionAccessRestriction.V1_ACTION_CONTEXT; + + private ActionAccessRestriction adminActionAccessRestriction; + + @Before + public void setup() { + adminActionAccessRestriction = applicationContext.getBean("adminActionAccessRestriction", AdminActionAccessRestriction.class); + } + + @Test + public void adminCanExecute() { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + Action action = createMailAction(); + adminActionAccessRestriction.verifyAccessRestriction(action); + } + + @Test + public void systemCanExecute() { + AuthenticationUtil.setRunAsUserSystem(); + + Action action = createMailAction(); + adminActionAccessRestriction.verifyAccessRestriction(action); + } + + @Test + public void userCantExecute() { + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getGuestUserName()); + + Action action = createMailAction(); + assertThrows(ActionAccessException.class, () -> adminActionAccessRestriction.verifyAccessRestriction(action)); + } + + private Action createMailAction() { + Map params = new HashMap<>(); + params.put("from", "admin@alfresco.com"); + params.put("to", "test@wp.pl"); + params.put("subject", "test"); + params.put("text", "test"); + + Action action = new ActionImpl(null, "123", MAIL_ACTION, params); + ActionAccessRestriction.setActionContext(action, CONTROLLED_CONTEXT); + + return action; + } +} diff --git a/repository/src/test/java/org/alfresco/repo/action/executer/AbstractMailActionExecuterTest.java b/repository/src/test/java/org/alfresco/repo/action/executer/AbstractMailActionExecuterTest.java index 7a029439fd..272048e132 100644 --- a/repository/src/test/java/org/alfresco/repo/action/executer/AbstractMailActionExecuterTest.java +++ b/repository/src/test/java/org/alfresco/repo/action/executer/AbstractMailActionExecuterTest.java @@ -1,954 +1,896 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ -package org.alfresco.repo.action.executer; - -import java.io.IOException; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import javax.mail.Address; -import javax.mail.Message; -import javax.mail.MessagingException; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeMessage; -import javax.mail.internet.MimeMessage.RecipientType; - -import org.alfresco.model.ContentModel; -import org.alfresco.repo.management.subsystems.ApplicationContextFactory; -import org.alfresco.repo.security.authentication.AuthenticationUtil; -import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; -import org.alfresco.repo.tenant.TenantService; -import org.alfresco.repo.tenant.TenantUtil; -import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork; -import org.alfresco.repo.transaction.RetryingTransactionHelper; -import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; -import org.alfresco.service.cmr.action.Action; -import org.alfresco.service.cmr.action.ActionService; -import org.alfresco.service.cmr.preference.PreferenceService; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.NodeService; -import org.alfresco.service.cmr.security.AuthorityService; -import org.alfresco.service.cmr.security.AuthorityType; -import org.alfresco.service.cmr.security.PermissionService; -import org.alfresco.service.cmr.security.PersonService; -import org.alfresco.service.namespace.QName; -import org.alfresco.service.transaction.TransactionService; -import org.alfresco.util.Pair; -import org.alfresco.util.PropertyMap; -import org.alfresco.util.test.junitrules.AlfrescoPerson; -import org.alfresco.util.test.junitrules.ApplicationContextInit; -import org.junit.Assert; -import org.junit.Test; -import org.springframework.context.ApplicationContext; - -/** - * Provides tests for the MailActionExecuter class. Most of this logic was in MailActionExecuterTest. - * Cloud code now includes extra tests and different rule config. - * Unfortunately this is messy due to the extensive use of static variables and Junit rules annotations. - * This class contains most of the test code, child classes actually setup the ClassRules and test fixtures, of - * particular importance is the static setupRuleChain() method in the child classes. The name is just a convention because - * it can't actually be enforced. The setupRuleChain() actually creates the users, as well as ordering the rules. - * You will see the AlfrescoPerson variables below are initialized as null, the assumption is that the child classes will - * create these users before they are needed (in the setupRuleChain() method), again this can't be enforced :(. - * - */ -public abstract class AbstractMailActionExecuterTest -{ - - // Rule to initialise the default Alfresco spring configuration - public static ApplicationContextInit APP_CONTEXT_INIT = new ApplicationContextInit(); - - // Rules to create test users, these are actually created in the setupRuleChain() method of child classes. - public static AlfrescoPerson BRITISH_USER = null; - public static AlfrescoPerson FRENCH_USER = null; - public static AlfrescoPerson AUSTRALIAN_USER = null; - public static AlfrescoPerson EXTERNAL_USER = null; - - private static String ALFRESCO_EE_USER = "plainUser"; - - protected static TransactionService TRANSACTION_SERVICE; - protected static ActionService ACTION_SERVICE; - protected static MailActionExecuter ACTION_EXECUTER; - protected static PreferenceService PREFERENCE_SERVICE; - protected static PersonService PERSON_SERVICE; - protected static AuthorityService AUTHORITY_SERVICE; - protected static NodeService NODE_SERVICE; - protected static PermissionService PERMISSION_SERVICE; - - protected static boolean WAS_IN_TEST_MODE; - - public static void setupTests(ApplicationContext appCtx) - { - TRANSACTION_SERVICE = appCtx.getBean("TransactionService", TransactionService.class); - ACTION_SERVICE = appCtx.getBean("ActionService", ActionService.class); - ACTION_EXECUTER = appCtx.getBean("OutboundSMTP", ApplicationContextFactory.class).getApplicationContext().getBean("mail", MailActionExecuter.class); - PREFERENCE_SERVICE = appCtx.getBean("PreferenceService", PreferenceService.class); - PERSON_SERVICE = appCtx.getBean("PersonService", PersonService.class); - NODE_SERVICE = appCtx.getBean("NodeService", NodeService.class); - AUTHORITY_SERVICE = appCtx.getBean("AuthorityService", AuthorityService.class); - PERMISSION_SERVICE = appCtx.getBean("PermissionService", PermissionService.class); - - WAS_IN_TEST_MODE = ACTION_EXECUTER.isTestMode(); - ACTION_EXECUTER.setTestMode(true); - - AuthenticationUtil.setRunAsUserSystem(); - - Map properties = new HashMap(1); - properties.put(ContentModel.PROP_USERNAME, ALFRESCO_EE_USER); - properties.put(ContentModel.PROP_EMAIL, "testemail@testdomain.com"); - PERSON_SERVICE.createPerson(properties, null); - - // All these test users are in the same tenant - either they're enterprise where there's only one, - // or they're cloud, where they have the same email domain - final String tenantId = getUsersHomeTenant(FRENCH_USER.getUsername()); - TenantUtil.runAsSystemTenant(new TenantRunAsWork() - { - @Override - public Object doWork() throws Exception - { - final Map preferences = new HashMap(); - - preferences.put("locale", "fr"); - PREFERENCE_SERVICE.setPreferences(FRENCH_USER.getUsername(), preferences); - - preferences.clear(); - preferences.put("locale", "en_GB"); - PREFERENCE_SERVICE.setPreferences(BRITISH_USER.getUsername(), preferences); - - preferences.clear(); - preferences.put("locale", "en_AU"); - PREFERENCE_SERVICE.setPreferences(AUSTRALIAN_USER.getUsername(), preferences); - - return null; - } - }, tenantId); - } - - private static String getUsersHomeTenant(String userName) - { - boolean thisIsCloud = false; - try - { - thisIsCloud = (Class.forName("org.alfresco.module.org_alfresco_module_cloud.registration.RegistrationService") != null); - } - catch (ClassNotFoundException ignoreIfThrown) - { - // Intentionally empty - } - - String result = TenantService.DEFAULT_DOMAIN; - - // Even if we get email address-style user names in an enterprise system, those are not to be given home tenants. - if (thisIsCloud) - { - String[] elems = userName.split("@"); - result = elems[1]; - } - - return result; - - } - - public static void tearDownTests() - { - ACTION_EXECUTER.setTestMode(WAS_IN_TEST_MODE); - PERSON_SERVICE.deletePerson(ALFRESCO_EE_USER); - } - - @Test - public void testUnknownRecipientUnknownSender() throws IOException, MessagingException - { - // PARAM_TO variant - Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); - mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TO, "some.bodyelse@example.com"); - - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, "alfresco/templates/mail/test.txt.ftl"); - - mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, (Serializable) getModel()); - - ACTION_SERVICE.executeAction(mailAction, null); - - MimeMessage message = ACTION_EXECUTER.retrieveLastTestMessage(); - Assert.assertNotNull(message); - Assert.assertEquals("Hello Jan 1, 1970", (String) message.getContent()); - } - - @Test - public void testUnknownRecipientUnknownSender_ToMany() throws IOException, MessagingException - { - // PARAM_TO_MANY variant - this code path currently has separate validation FIXME fix this. - Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); - mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TO_MANY, "some.bodyelse@example.com"); - - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, "alfresco/templates/mail/test.txt.ftl"); - - mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, (Serializable) getModel()); - - ACTION_SERVICE.executeAction(mailAction, null); - - MimeMessage message = ACTION_EXECUTER.retrieveLastTestMessage(); - Assert.assertNotNull(message); - Assert.assertEquals("Hello Jan 1, 1970", (String) message.getContent()); - } - - private Serializable getModel() - { - Map model = new HashMap(); - - model.put("epoch", new Date(0)); - return (Serializable) model; - } - - @Test - public void testFrenchRecipient() throws IOException, MessagingException - { - String from = "some.body@example.com"; - Serializable recipients = (Serializable) Arrays.asList(FRENCH_USER.getUsername()); - String subject = ""; - String template = "alfresco/templates/mail/test.txt.ftl"; - - MimeMessage message = sendMessage(from, recipients, subject, template); - - Assert.assertNotNull(message); - Assert.assertEquals("Bonjour 1 janv. 1970", (String) message.getContent()); - } - - @Test - public void testHTMLDetection() throws IOException, MessagingException - { - String from = "some.body@example.com"; - Serializable recipients = (Serializable) Arrays.asList(FRENCH_USER.getUsername()); - String subject = ""; - - Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); - mailAction.setParameterValue(MailActionExecuter.PARAM_TO_MANY, recipients); - - // First with plain text - String text = "This is plain text\nOnly\nBut it mentions HTML and "; - - MimeMessage message = sendMessage(from, subject, null, text, mailAction); - - Assert.assertNotNull(message); - Assert.assertEquals(text, (String) message.getContent()); - Assert.assertEquals("text/plain", // Ignore charset - message.getDataHandler().getContentType().substring(0, 10)); - - // HTML opening tag - text = "HTML emails are great"; - message = sendMessage(from, subject, null, text, mailAction); - - Assert.assertNotNull(message); - Assert.assertEquals(text, (String) message.getContent()); - Assert.assertEquals("text/html", // Ignore charset - message.getDataHandler().getContentType().substring(0, 9)); - - // HTML Doctype - text = "\n\nMore complex HTML"; - message = sendMessage(from, subject, null, text, mailAction); - - Assert.assertNotNull(message); - Assert.assertEquals(text, (String) message.getContent()); - Assert.assertEquals("text/html", // Ignore charset - message.getDataHandler().getContentType().substring(0, 9)); - } - - protected MimeMessage sendMessage(String from, Serializable recipients, String subject, String template) - { - Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); - mailAction.setParameterValue(MailActionExecuter.PARAM_TO_MANY, recipients); - - return sendMessage(from, subject, template, mailAction); - } - - protected MimeMessage sendMessage(String from, String subject, String template, final Action mailAction) - { - return sendMessage(from, subject, template, null, mailAction); - } - protected MimeMessage sendMessage(String from, String subject, String template, String bodyText, final Action mailAction) - { - if (from != null) - { - mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, from); - } - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, subject); - if (template != null) - { - mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, template); - mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, getModel()); - } - else - { - mailAction.setParameterValue(MailActionExecuter.PARAM_TEXT, bodyText); - } - - RetryingTransactionHelper txHelper = APP_CONTEXT_INIT.getApplicationContext().getBean("retryingTransactionHelper", RetryingTransactionHelper.class); - - return txHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public MimeMessage execute() throws Throwable - { - ACTION_SERVICE.executeAction(mailAction, null); - - return ACTION_EXECUTER.retrieveLastTestMessage(); - } - }, true); - } - - protected MimeMessage sendMessage(String from, String to, String subject, String template) - { - Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); - mailAction.setParameterValue(MailActionExecuter.PARAM_TO, to); - return sendMessage(from, subject, template, mailAction); - } - - /** - * Test for ALF-19231 - */ - @Test - public void testSendMailActionForUserNameAsRecipient() throws IOException, MessagingException - { - String from = BRITISH_USER.getUsername(); - Serializable recipients = (Serializable) Arrays.asList(ALFRESCO_EE_USER); - String subject = "Testing"; - String template = "alfresco/templates/mail/test.txt.ftl"; - - MimeMessage message = sendMessage(from, recipients, subject, template); - - Assert.assertNotNull(message); - Assert.assertEquals("Hello 1 Jan 1970", (String) message.getContent()); - } - - @Test - public void testUnknowRecipientAustralianSender() throws IOException, MessagingException - { - String from = AUSTRALIAN_USER.getUsername(); - String to = "some.body@example.com"; - String subject = "Testing"; - String template = "alfresco/templates/mail/test.txt.ftl"; - - MimeMessage message = sendMessage(from, to, subject, template); - - Assert.assertNotNull(message); - Assert.assertEquals("G'Day 1 Jan. 1970", (String) message.getContent()); - } - - @Test - public void testSendingTestMessageWithNoCurrentUser() - { - try - { - // run with no current user - AuthenticationUtil.clearCurrentSecurityContext(); - - final Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); - mailAction.setParameterValue(MailActionExecuter.PARAM_TO, "some.body@eaxmple.com"); - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TEXT, "This is a test message."); - - TRANSACTION_SERVICE.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() - { - @Override - public Void execute() throws Throwable - { - ACTION_EXECUTER.executeImpl(mailAction, null); - return null; - } - }); - } - finally - { - // restore system user as current user - AuthenticationUtil.setRunAsUserSystem(); - } - } - - @Test - public void testPrepareEmailForDisabledUsers() throws MessagingException - { - String groupName = null; - final String USER1 = "test_user1"; - final String USER2 = "test_user2"; - try - { - createUser(USER1, null); - NodeRef userNode = createUser(USER2, null); - groupName = AUTHORITY_SERVICE.createAuthority(AuthorityType.GROUP, "testgroup1"); - AUTHORITY_SERVICE.addAuthority(groupName, USER1); - AUTHORITY_SERVICE.addAuthority(groupName, USER2); - NODE_SERVICE.addAspect(userNode, ContentModel.ASPECT_PERSON_DISABLED, null); - final Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); - mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TO_MANY, groupName); - - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TEXT, "Testing"); - - RetryingTransactionHelper txHelper = APP_CONTEXT_INIT.getApplicationContext().getBean("retryingTransactionHelper", RetryingTransactionHelper.class); - - MimeMessage mm = txHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public MimeMessage execute() throws Throwable - { - return ACTION_EXECUTER.prepareEmail(mailAction, null, null, null).getMimeMessage(); - } - }, true); - - Address[] addresses = mm.getRecipients(Message.RecipientType.TO); - Assert.assertEquals(1, addresses.length); - Assert.assertEquals(USER1 + "@email.com", addresses[0].toString()); - } - finally - { - if (groupName != null) - { - AUTHORITY_SERVICE.deleteAuthority(groupName, true); - } - PERSON_SERVICE.deletePerson(USER1); - PERSON_SERVICE.deletePerson(USER2); - } - } - - @Test - public void testPrepareEmailSubjectParams() - { - Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); - mailAction.setParameterValue(MailActionExecuter.PARAM_TO, "some.bodyelse@example.com"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, "alfresco/templates/mail/test.txt.ftl"); - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Test Subject Params"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, getModel()); - Pair recipient = new Pair("test", Locale.ENGLISH); - - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT_PARAMS, new Object[] {"Test", "Subject", "Params", "Object", "Array"}); - Assert.assertNotNull("We should support Object[] value for PARAM_SUBJECT_PARAMS", ACTION_EXECUTER.prepareEmail(mailAction, null, recipient, null)); - - ArrayList params = new ArrayList(); - params.add("Test"); - params.add("Subject"); - params.add("Params"); - params.add("ArrayList"); - - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT_PARAMS, params); - Assert.assertNotNull("We should support List value for PARAM_SUBJECT_PARAMS", ACTION_EXECUTER.prepareEmail(mailAction, null, recipient, null)); - - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT_PARAMS, "Test Subject Params Single String"); - Assert.assertNotNull("We should support String value for PARAM_SUBJECT_PARAMS", ACTION_EXECUTER.prepareEmail(mailAction, null, recipient, null)); - - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT_PARAMS, null); - Assert.assertNotNull("We should support null value for PARAM_SUBJECT_PARAMS", ACTION_EXECUTER.prepareEmail(mailAction, null, recipient, null)); - } - - /** - * Creates a test user with the specified username and optionally custom email. - * - * @param userName String - * @param email Optional, if not specified assigned to userName + "@email.com" - * @return NodeRef - */ - private NodeRef createUser(String userName, String email) - { - PropertyMap personProps = new PropertyMap(); - personProps.put(ContentModel.PROP_USERNAME, userName); - personProps.put(ContentModel.PROP_FIRSTNAME, userName); - personProps.put(ContentModel.PROP_LASTNAME, userName); - if (email != null) - { - personProps.put(ContentModel.PROP_EMAIL, email); - } - else - { - personProps.put(ContentModel.PROP_EMAIL, userName + "@email.com"); - } - - return PERSON_SERVICE.createPerson(personProps); - } - - /** - * Test for MNT-10874 - * @throws Exception - */ - @Test - public void testUserWithNonExistingTenant() throws Exception - { - final String USER_WITH_NON_EXISTING_TENANT = "test_user_non_tenant@non_existing_tenant.com"; - - try - { - createUser(USER_WITH_NON_EXISTING_TENANT, USER_WITH_NON_EXISTING_TENANT); - - final Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); - mailAction.setParameterValue(MailActionExecuter.PARAM_TO, USER_WITH_NON_EXISTING_TENANT); - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TEXT, "This is a test message."); - - // run as non admin and non system - AuthenticationUtil.setFullyAuthenticatedUser(BRITISH_USER.getUsername()); - TRANSACTION_SERVICE.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() - { - @Override - public Void execute() throws Throwable - { - ACTION_EXECUTER.executeImpl(mailAction, null); - return null; - } - }); - } - finally - { - // restore system user as current user - AuthenticationUtil.setRunAsUserSystem(); - // tidy up - PERSON_SERVICE.deletePerson(USER_WITH_NON_EXISTING_TENANT); - AuthenticationUtil.clearCurrentSecurityContext(); - } - } - - /** - * Test for MNT-11488 - * @throws IOException - * @throws MessagingException - */ - @Test - public void testSendingToMultipleUsers() throws IOException, MessagingException - { - final String USER_1 = "recipient1"; - final String USER_2 = "recipient2"; - final String[] recipientsArray = { USER_1 + "@email.com", USER_2 + "@email.com" }; - final List recipientsResult = new ArrayList(Arrays.asList(recipientsArray)) ; - - try - { - createUser(USER_1, null); - createUser(USER_2, null); - ArrayList recipients = new ArrayList(2); - recipients.add(USER_1); - recipients.add(USER_2); - - Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); - mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "sender@example.com"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TO_MANY, recipients); - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, (Serializable) getModel()); - - ACTION_EXECUTER.resetTestSentCount(); - - ACTION_SERVICE.executeAction(mailAction, null); - - MimeMessage message = ACTION_EXECUTER.retrieveLastTestMessage(); - Assert.assertNotNull(message); - Assert.assertEquals("One email should be sent", 1, ACTION_EXECUTER.getTestSentCount()); - Assert.assertEquals("All recipients should receive single message", 2, message.getAllRecipients().length); - - Assert.assertTrue("Both users should receive message", recipientsResult.contains(((InternetAddress) message.getAllRecipients()[0]).getAddress())); - Assert.assertTrue("Both users should receive message", recipientsResult.contains(((InternetAddress) message.getAllRecipients()[1]).getAddress())); - } - finally - { - // tidy up - PERSON_SERVICE.deletePerson(USER_1); - PERSON_SERVICE.deletePerson(USER_2); - } - } - - /** - * Test for CC / BCC - * @throws Exception - */ - @Test - public void testSendingToCarbonCopy() throws IOException, MessagingException - { - // PARAM_TO variant - Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); - mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TO, "some.bodyelse@example.com"); - mailAction.setParameterValue(MailActionExecuter.PARAM_CC, "some.carbon@example.com"); - mailAction.setParameterValue(MailActionExecuter.PARAM_BCC, "some.blindcarbon@example.com"); - - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing CARBON COPY"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, "alfresco/templates/mail/test.txt.ftl"); - - mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, (Serializable) getModel()); - - ACTION_SERVICE.executeAction(mailAction, null); - - MimeMessage message = ACTION_EXECUTER.retrieveLastTestMessage(); - Assert.assertNotNull(message); - Address[] all = message.getAllRecipients(); - Address[] ccs = message.getRecipients(RecipientType.CC); - Address[] bccs = message.getRecipients(RecipientType.BCC); - Assert.assertEquals(3, all.length); - Assert.assertEquals(1, ccs.length); - Assert.assertEquals(1, bccs.length); - Assert.assertTrue(ccs[0].toString().contains("some.carbon")); - Assert.assertTrue(bccs[0].toString().contains("some.blindcarbon")); - } - - /** - * Test for MNT-11079 - */ - @Test - public void testSendingToUserWithMailAlikeName() throws IOException, MessagingException - { - final String USER_1 = "user1@namelookslikeemail"; - final String USER_1_EMAIL = "user1@trueemail.com"; - - try - { - createUser(USER_1, USER_1_EMAIL); - - Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); - mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TO_MANY, USER_1); - - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, "alfresco/templates/mail/test.txt.ftl"); - - mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, (Serializable) getModel()); - - ACTION_SERVICE.executeAction(mailAction, null); - - MimeMessage message = ACTION_EXECUTER.retrieveLastTestMessage(); - Assert.assertNotNull(message); - Assert.assertEquals("Hello Jan 1, 1970", (String) message.getContent()); - Assert.assertEquals(1, message.getAllRecipients().length); - javax.mail.internet.InternetAddress address = (InternetAddress) message.getAllRecipients()[0]; - Assert.assertEquals(USER_1_EMAIL, address.getAddress()); - } - finally - { - // tidy up - PERSON_SERVICE.deletePerson(USER_1); - } - } - - /** - * Test for MNT-12464 - * @throws Exception - */ - @Test - public void testMultipleIdenticalEmailsToUser() throws Exception - { - final String USER_1 = "user12464_1"; - final String USER_2 = "user12464_2"; - final String USER_3 = "user12464_3"; - final String USER_6 = "user12464_6"; - - final String USER_4_USERNAME = "user12464_4@mnt12464mail.com"; - final String USER_5_USERNAME = "user12464_5@mnt12464mail.com"; - - String[] users = new String[] { USER_1, USER_2, USER_3, USER_4_USERNAME, USER_5_USERNAME }; - - final String GROUP_1_SHORT_NAME = "mnt12464group1"; - final String GROUP_1 = "GROUP_" + GROUP_1_SHORT_NAME; - final String GROUP_2_SHORT_NAME = "mnt12464group2"; - final String GROUP_2 = "GROUP_" + GROUP_2_SHORT_NAME; - - try - { - createUser(USER_1, null); - createUser(USER_2, null); - createUser(USER_3, null); - AUTHORITY_SERVICE.createAuthority(AuthorityType.GROUP, GROUP_1_SHORT_NAME); - AUTHORITY_SERVICE.createAuthority(AuthorityType.GROUP, GROUP_2_SHORT_NAME); - AUTHORITY_SERVICE.addAuthority(GROUP_1, USER_1); - AUTHORITY_SERVICE.addAuthority(GROUP_1, USER_2); - AUTHORITY_SERVICE.addAuthority(GROUP_2, USER_1); - AUTHORITY_SERVICE.addAuthority(GROUP_2, USER_2); - AUTHORITY_SERVICE.addAuthority(GROUP_2, USER_3); - - // these persons should be without emails - PropertyMap personProps = new PropertyMap(); - personProps.put(ContentModel.PROP_USERNAME, USER_4_USERNAME); - personProps.put(ContentModel.PROP_FIRSTNAME, USER_4_USERNAME); - personProps.put(ContentModel.PROP_LASTNAME, USER_4_USERNAME); - PERSON_SERVICE.createPerson(personProps); - AUTHORITY_SERVICE.addAuthority(GROUP_1, USER_4_USERNAME); - AUTHORITY_SERVICE.addAuthority(GROUP_2, USER_4_USERNAME); - - personProps = new PropertyMap(); - personProps.put(ContentModel.PROP_USERNAME, USER_5_USERNAME); - personProps.put(ContentModel.PROP_FIRSTNAME, USER_5_USERNAME); - personProps.put(ContentModel.PROP_LASTNAME, USER_5_USERNAME); - PERSON_SERVICE.createPerson(personProps); - - Action mailAction1 = ACTION_SERVICE.createAction(MailActionExecuter.NAME); - mailAction1.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); - ArrayList toMany1 = new ArrayList(); - toMany1.add(USER_1); - toMany1.add(GROUP_1); - toMany1.add(USER_2); - toMany1.add(GROUP_2); - toMany1.add(USER_3); - toMany1.add(USER_4_USERNAME); - toMany1.add(USER_5_USERNAME); - mailAction1.setParameterValue(MailActionExecuter.PARAM_TO_MANY, toMany1); - mailAction1.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing MNT-12464"); - mailAction1.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, "alfresco/templates/mail/test.txt.ftl"); - mailAction1.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, (Serializable) getModel()); - - ACTION_EXECUTER.resetTestSentCount(); - ACTION_SERVICE.executeAction(mailAction1, null); - - Assert.assertEquals("Must be received one letter on each recipient", users.length, ACTION_EXECUTER.getTestSentCount()); - - // testing for GROUP_EVERYONE - - ACTION_EXECUTER.resetTestSentCount(); - everyoneSending(); - int before = ACTION_EXECUTER.getTestSentCount(); - // create one additional user - NodeRef user6 = createUser(USER_6, null); - PERMISSION_SERVICE.setInheritParentPermissions(user6, false); - PERMISSION_SERVICE.deletePermissions(user6); - - // USER_6 not exist for USER_1, but he will be added to recipients - int after = AuthenticationUtil.runAs(new RunAsWork() - { - @Override - public Integer doWork() throws Exception - { - ACTION_EXECUTER.resetTestSentCount(); - everyoneSending(); - return ACTION_EXECUTER.getTestSentCount(); - } - }, USER_1); - - Assert.assertEquals("One additional user was created, quantity of recipients GROUP_EVERYONE must be +1 user", 1, after - before); - } - finally - { - PERSON_SERVICE.deletePerson(USER_1); - PERSON_SERVICE.deletePerson(USER_2); - PERSON_SERVICE.deletePerson(USER_3); - PERSON_SERVICE.deletePerson(USER_4_USERNAME); - PERSON_SERVICE.deletePerson(USER_5_USERNAME); - PERSON_SERVICE.deletePerson(USER_6); - AUTHORITY_SERVICE.deleteAuthority(GROUP_1); - AUTHORITY_SERVICE.deleteAuthority(GROUP_2); - } - } - - private void everyoneSending() - { - Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); - mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); - ArrayList toMany = new ArrayList(); - toMany.add(PERMISSION_SERVICE.getAllAuthorities()); - mailAction.setParameterValue(MailActionExecuter.PARAM_TO_MANY, toMany); - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing MNT-12464"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, "alfresco/templates/mail/test.txt.ftl"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, (Serializable) getModel()); - - ACTION_EXECUTER.resetTestSentCount(); - - ACTION_SERVICE.executeAction(mailAction, null); - } - - /** - * ACE-2564 - */ - @Test - public void testSendEmailByExternalUser() throws IOException, MessagingException - { - final Serializable recipients = (Serializable) Arrays.asList(BRITISH_USER.getUsername()); - final String subject = ""; - final String template = "alfresco/templates/mail/test.txt.ftl"; - AuthenticationUtil.pushAuthentication(); - AuthenticationUtil.setFullyAuthenticatedUser(EXTERNAL_USER.getUsername()); - MimeMessage message = null; - - - try - { - - // these persons should be without emails - // testing for GROUP_EVERYONE - - final String tenantId = getUsersHomeTenant(BRITISH_USER.getUsername()); - - // USER_6 not exist for USER_1, but he will be added to recipients - message = TenantUtil.runAsTenant(new TenantRunAsWork() - { - @Override - public MimeMessage doWork() throws Exception - { - return sendMessage(null, recipients, subject, template); - } - }, tenantId); - - Assert.assertNotNull(message); - Assert.assertEquals("Hello 1 Jan 1970", (String) message.getContent()); - } - finally - { - AuthenticationUtil.popAuthentication(); - } - } - - /** - * Test for MNT-17970 - * @throws IOException - * @throws MessagingException - */ - @Test - public void testGetToUsersWhenSendingToGroup() throws IOException, MessagingException - { - String groupName = null; - final String USER1 = "test_user1"; - final String USER2 = "test_user2"; - try - { - // Create users and add them to a group - createUser(USER1, null); - createUser(USER2, null); - groupName = AUTHORITY_SERVICE.createAuthority(AuthorityType.GROUP, "testgroup1"); - AUTHORITY_SERVICE.addAuthority(groupName, USER1); - AUTHORITY_SERVICE.addAuthority(groupName, USER2); - - // Create mail - final Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); - mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TO_MANY, groupName); - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TEXT, "Testing"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, "alfresco/templates/mail/testSentTo.txt.ftl"); - - RetryingTransactionHelper txHelper = APP_CONTEXT_INIT.getApplicationContext().getBean("retryingTransactionHelper", RetryingTransactionHelper.class); - - // Send mail - MimeMessage message = txHelper.doInTransaction(new RetryingTransactionCallback() - { - @Override - public MimeMessage execute() throws Throwable - { - ACTION_EXECUTER.executeImpl(mailAction, null); - return ACTION_EXECUTER.retrieveLastTestMessage(); - } - }, true); - - // Check that both users are displayed in message body - String recipients = USER1 + "@email.com" + "," + USER2 + "@email.com"; - Assert.assertNotNull(message); - Assert.assertEquals("This email was sent to " + recipients, (String) message.getContent()); - } - finally - { - if (groupName != null) - { - AUTHORITY_SERVICE.deleteAuthority(groupName, true); - } - PERSON_SERVICE.deletePerson(USER1); - PERSON_SERVICE.deletePerson(USER2); - } - } - - /** - * ALF-21948 - */ - @Test - public void testSendingToArrayOfCarbonCopyAndBlindCarbonCopyUsers() throws MessagingException - { - Map params = new HashMap(); - String[] ccArray = { "cc_user1@example.com", "cc_user2@example.com" }; - String[] bccArray = { "bcc_user3@example.com", "bcc_user4@example.com", "bcc_user5@example.com" }; - params.put(MailActionExecuter.PARAM_FROM, "sender@email.com"); - params.put(MailActionExecuter.PARAM_TO, "test@email.com"); - params.put(MailActionExecuter.PARAM_CC, ccArray); - params.put(MailActionExecuter.PARAM_BCC, bccArray); - - params.put(MailActionExecuter.PARAM_TEXT, "Mail body here"); - params.put(MailActionExecuter.PARAM_SUBJECT, "Subject text"); - - Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME, params); - ACTION_EXECUTER.resetTestSentCount(); - - ACTION_SERVICE.executeAction(mailAction, null); - MimeMessage message = ACTION_EXECUTER.retrieveLastTestMessage(); - Assert.assertNotNull(message); - - Address[] all = message.getAllRecipients(); - Address[] ccs = message.getRecipients(RecipientType.CC); - Address[] bccs = message.getRecipients(RecipientType.BCC); - Assert.assertEquals(6, all.length); - Assert.assertEquals(2, ccs.length); - Assert.assertEquals(3, bccs.length); - Assert.assertTrue(ccs[0].toString().contains("cc_user1") && ccs[1].toString().contains("cc_user2")); - Assert.assertTrue(bccs[0].toString().contains("bcc_user3") && bccs[1].toString().contains("bcc_user4") - && bccs[2].toString().contains("bcc_user5")); - } - - /** - * ALF-21948 - */ - @Test - public void testSendingToListOfCarbonCopyAndBlindCarbonCopyUsers() throws MessagingException - { - List ccList = new ArrayList(); - ccList.add("cc_user1@example.com"); - ccList.add("cc_user2@example.com"); - - List bccList = new ArrayList(); - bccList.add("bcc_user3@example.com"); - bccList.add("bcc_user4@example.com"); - bccList.add("bcc_user5@example.com"); - - Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); - mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TO, "some.bodyelse@example.com"); - mailAction.setParameterValue(MailActionExecuter.PARAM_CC, (Serializable) ccList); - mailAction.setParameterValue(MailActionExecuter.PARAM_BCC, (Serializable) bccList); - - mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing (BLIND) CARBON COPY"); - mailAction.setParameterValue(MailActionExecuter.PARAM_TEXT, "mail body here"); - - ACTION_EXECUTER.resetTestSentCount(); - ACTION_SERVICE.executeAction(mailAction, null); - MimeMessage message = ACTION_EXECUTER.retrieveLastTestMessage(); - Assert.assertNotNull(message); - - Address[] all = message.getAllRecipients(); - Address[] ccs = message.getRecipients(RecipientType.CC); - Address[] bccs = message.getRecipients(RecipientType.BCC); - Assert.assertEquals(6, all.length); - Assert.assertEquals(2, ccs.length); - Assert.assertEquals(3, bccs.length); - Assert.assertTrue(ccs[0].toString().contains("cc_user1") && ccs[1].toString().contains("cc_user2")); - Assert.assertTrue(bccs[0].toString().contains("bcc_user3") && bccs[1].toString().contains("bcc_user4") - && bccs[2].toString().contains("bcc_user5")); - } - -} +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.action.executer; + +import javax.mail.Address; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMessage.RecipientType; +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.management.subsystems.ApplicationContextFactory; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.tenant.TenantService; +import org.alfresco.repo.tenant.TenantUtil; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionService; +import org.alfresco.service.cmr.preference.PreferenceService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.security.AuthorityType; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.Pair; +import org.alfresco.util.PropertyMap; +import org.alfresco.util.test.junitrules.AlfrescoPerson; +import org.alfresco.util.test.junitrules.ApplicationContextInit; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.context.ApplicationContext; + +/** + * Provides tests for the MailActionExecuter class. Most of this logic was in MailActionExecuterTest. + * Cloud code now includes extra tests and different rule config. + * Unfortunately this is messy due to the extensive use of static variables and Junit rules annotations. + * This class contains most of the test code, child classes actually set up the ClassRules and test fixtures, of + * particular importance is the static setupRuleChain() method in the child classes. The name is just a convention because + * it can't actually be enforced. The setupRuleChain() actually creates the users, as well as ordering the rules. + * You will see the AlfrescoPerson variables below are initialized as null, the assumption is that the child classes will + * create these users before they are needed (in the setupRuleChain() method), again this can't be enforced :(. + * + */ +public abstract class AbstractMailActionExecuterTest +{ + + // Rule to initialise the default Alfresco spring configuration + public static ApplicationContextInit APP_CONTEXT_INIT = new ApplicationContextInit(); + + // Rules to create test users, these are actually created in the setupRuleChain() method of child classes. + public static AlfrescoPerson BRITISH_USER = null; + public static AlfrescoPerson FRENCH_USER = null; + public static AlfrescoPerson AUSTRALIAN_USER = null; + public static AlfrescoPerson EXTERNAL_USER = null; + + private static final String ALFRESCO_EE_USER = "plainUser"; + + protected static TransactionService TRANSACTION_SERVICE; + protected static ActionService ACTION_SERVICE; + protected static MailActionExecuter ACTION_EXECUTER; + protected static PreferenceService PREFERENCE_SERVICE; + protected static PersonService PERSON_SERVICE; + protected static AuthorityService AUTHORITY_SERVICE; + protected static NodeService NODE_SERVICE; + protected static PermissionService PERMISSION_SERVICE; + + protected static boolean WAS_IN_TEST_MODE; + + public static void setupTests(ApplicationContext appCtx) + { + TRANSACTION_SERVICE = appCtx.getBean("TransactionService", TransactionService.class); + ACTION_SERVICE = appCtx.getBean("ActionService", ActionService.class); + ACTION_EXECUTER = appCtx.getBean("OutboundSMTP", ApplicationContextFactory.class).getApplicationContext().getBean("mail", MailActionExecuter.class); + PREFERENCE_SERVICE = appCtx.getBean("PreferenceService", PreferenceService.class); + PERSON_SERVICE = appCtx.getBean("PersonService", PersonService.class); + NODE_SERVICE = appCtx.getBean("NodeService", NodeService.class); + AUTHORITY_SERVICE = appCtx.getBean("AuthorityService", AuthorityService.class); + PERMISSION_SERVICE = appCtx.getBean("PermissionService", PermissionService.class); + + WAS_IN_TEST_MODE = ACTION_EXECUTER.isTestMode(); + ACTION_EXECUTER.setTestMode(true); + + AuthenticationUtil.setRunAsUserSystem(); + + Map properties = new HashMap<>(1); + properties.put(ContentModel.PROP_USERNAME, ALFRESCO_EE_USER); + properties.put(ContentModel.PROP_EMAIL, "testemail@testdomain.com"); + PERSON_SERVICE.createPerson(properties, null); + + // All these test users are in the same tenant - either they're enterprise where there's only one, + // or they're cloud, where they have the same email domain + final String tenantId = getUsersHomeTenant(FRENCH_USER.getUsername()); + TenantUtil.runAsSystemTenant(() -> { + final Map preferences = new HashMap<>(); + + preferences.put("locale", "fr"); + PREFERENCE_SERVICE.setPreferences(FRENCH_USER.getUsername(), preferences); + + preferences.clear(); + preferences.put("locale", "en_GB"); + PREFERENCE_SERVICE.setPreferences(BRITISH_USER.getUsername(), preferences); + + preferences.clear(); + preferences.put("locale", "en_AU"); + PREFERENCE_SERVICE.setPreferences(AUSTRALIAN_USER.getUsername(), preferences); + + return null; + }, tenantId); + } + + private static String getUsersHomeTenant(String userName) + { + boolean thisIsCloud = false; + try + { + thisIsCloud = (Class.forName("org.alfresco.module.org_alfresco_module_cloud.registration.RegistrationService") != null); + } + catch (ClassNotFoundException ignoreIfThrown) + { + // Intentionally empty + } + + String result = TenantService.DEFAULT_DOMAIN; + + // Even if we get email address-style usernames in an enterprise system, those are not to be given home tenants. + if (thisIsCloud) + { + String[] elems = userName.split("@"); + result = elems[1]; + } + + return result; + + } + + public static void tearDownTests() + { + ACTION_EXECUTER.setTestMode(WAS_IN_TEST_MODE); + PERSON_SERVICE.deletePerson(ALFRESCO_EE_USER); + } + + @Test + public void testUnknownRecipientUnknownSender() throws IOException, MessagingException + { + // PARAM_TO variant + Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); + mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TO, "some.bodyelse@example.com"); + + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, "alfresco/templates/mail/test.txt.ftl"); + + mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, getModel()); + + ACTION_SERVICE.executeAction(mailAction, null); + + MimeMessage message = ACTION_EXECUTER.retrieveLastTestMessage(); + Assert.assertNotNull(message); + Assert.assertEquals("Hello Jan 1, 1970", message.getContent()); + } + + @Test + public void testUnknownRecipientUnknownSender_ToMany() throws IOException, MessagingException + { + // PARAM_TO_MANY variant - this code path currently has separate validation FIXME fix this. + Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); + mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TO_MANY, "some.bodyelse@example.com"); + + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, "alfresco/templates/mail/test.txt.ftl"); + + mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, getModel()); + + ACTION_SERVICE.executeAction(mailAction, null); + + MimeMessage message = ACTION_EXECUTER.retrieveLastTestMessage(); + Assert.assertNotNull(message); + Assert.assertEquals("Hello Jan 1, 1970", message.getContent()); + } + + private Serializable getModel() + { + Map model = new HashMap<>(); + + model.put("epoch", new Date(0)); + return (Serializable) model; + } + + @Test + public void testFrenchRecipient() throws IOException, MessagingException + { + String from = "some.body@example.com"; + Serializable recipients = (Serializable) Arrays.asList(FRENCH_USER.getUsername()); + String subject = ""; + String template = "alfresco/templates/mail/test.txt.ftl"; + + MimeMessage message = sendMessage(from, recipients, subject, template); + + Assert.assertNotNull(message); + Assert.assertEquals("Bonjour 1 janv. 1970", message.getContent()); + } + + @Test + public void testHTMLDetection() throws IOException, MessagingException + { + String from = "some.body@example.com"; + Serializable recipients = (Serializable) Arrays.asList(FRENCH_USER.getUsername()); + String subject = ""; + + Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); + mailAction.setParameterValue(MailActionExecuter.PARAM_TO_MANY, recipients); + + // First with plain text + String text = "This is plain text\nOnly\nBut it mentions HTML and "; + + MimeMessage message = sendMessage(from, subject, null, text, mailAction); + + Assert.assertNotNull(message); + Assert.assertEquals(text, message.getContent()); + Assert.assertEquals("text/plain", // Ignore charset + message.getDataHandler().getContentType().substring(0, 10)); + + // HTML opening tag + text = "HTML emails are great"; + message = sendMessage(from, subject, null, text, mailAction); + + Assert.assertNotNull(message); + Assert.assertEquals(text, message.getContent()); + Assert.assertEquals("text/html", // Ignore charset + message.getDataHandler().getContentType().substring(0, 9)); + + // HTML Doctype + text = "\n\nMore complex HTML"; + message = sendMessage(from, subject, null, text, mailAction); + + Assert.assertNotNull(message); + Assert.assertEquals(text, message.getContent()); + Assert.assertEquals("text/html", // Ignore charset + message.getDataHandler().getContentType().substring(0, 9)); + } + + protected MimeMessage sendMessage(String from, Serializable recipients, String subject, String template) + { + Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); + mailAction.setParameterValue(MailActionExecuter.PARAM_TO_MANY, recipients); + + return sendMessage(from, subject, template, mailAction); + } + + protected MimeMessage sendMessage(String from, String subject, String template, final Action mailAction) + { + return sendMessage(from, subject, template, null, mailAction); + } + protected MimeMessage sendMessage(String from, String subject, String template, String bodyText, final Action mailAction) + { + if (from != null) + { + mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, from); + } + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, subject); + if (template != null) + { + mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, template); + mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, getModel()); + } + else + { + mailAction.setParameterValue(MailActionExecuter.PARAM_TEXT, bodyText); + } + + RetryingTransactionHelper txHelper = APP_CONTEXT_INIT.getApplicationContext().getBean("retryingTransactionHelper", RetryingTransactionHelper.class); + + return txHelper.doInTransaction(() -> { + ACTION_SERVICE.executeAction(mailAction, null); + return ACTION_EXECUTER.retrieveLastTestMessage(); + }, true); + } + + protected MimeMessage sendMessage(String from, String to, String subject, String template) + { + Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); + mailAction.setParameterValue(MailActionExecuter.PARAM_TO, to); + return sendMessage(from, subject, template, mailAction); + } + + /** + * Test for ALF-19231 + */ + @Test + public void testSendMailActionForUserNameAsRecipient() throws IOException, MessagingException + { + String from = BRITISH_USER.getUsername(); + Serializable recipients = (Serializable) Arrays.asList(ALFRESCO_EE_USER); + String subject = "Testing"; + String template = "alfresco/templates/mail/test.txt.ftl"; + + MimeMessage message = sendMessage(from, recipients, subject, template); + + Assert.assertNotNull(message); + Assert.assertEquals("Hello 1 Jan 1970", message.getContent()); + } + + @Test + public void testUnknownRecipientAustralianSender() throws IOException, MessagingException + { + String from = AUSTRALIAN_USER.getUsername(); + String to = "some.body@example.com"; + String subject = "Testing"; + String template = "alfresco/templates/mail/test.txt.ftl"; + + MimeMessage message = sendMessage(from, to, subject, template); + + Assert.assertNotNull(message); + Assert.assertEquals("G'Day 1 Jan 1970", message.getContent()); + } + + @Test + public void testSendingTestMessageWithNoCurrentUser() + { + try + { + // run with no current user + AuthenticationUtil.clearCurrentSecurityContext(); + + final Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); + mailAction.setParameterValue(MailActionExecuter.PARAM_TO, "some.body@eaxmple.com"); + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TEXT, "This is a test message."); + + TRANSACTION_SERVICE.getRetryingTransactionHelper().doInTransaction( + (RetryingTransactionCallback) () -> { + ACTION_EXECUTER.executeImpl(mailAction, null); + return null; + }); + } + finally + { + // restore system user as current user + AuthenticationUtil.setRunAsUserSystem(); + } + } + + @Test + public void testPrepareEmailForDisabledUsers() throws MessagingException + { + String groupName = null; + final String USER1 = "test_user1"; + final String USER2 = "test_user2"; + try + { + createUser(USER1, null); + NodeRef userNode = createUser(USER2, null); + groupName = AUTHORITY_SERVICE.createAuthority(AuthorityType.GROUP, "testgroup1"); + AUTHORITY_SERVICE.addAuthority(groupName, USER1); + AUTHORITY_SERVICE.addAuthority(groupName, USER2); + NODE_SERVICE.addAspect(userNode, ContentModel.ASPECT_PERSON_DISABLED, null); + final Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); + mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TO_MANY, groupName); + + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TEXT, "Testing"); + + RetryingTransactionHelper txHelper = APP_CONTEXT_INIT.getApplicationContext().getBean("retryingTransactionHelper", RetryingTransactionHelper.class); + + MimeMessage mm = txHelper.doInTransaction( + () -> ACTION_EXECUTER.prepareEmail(mailAction, null, null, null).getMimeMessage(), true); + + Address[] addresses = mm.getRecipients(Message.RecipientType.TO); + Assert.assertEquals(1, addresses.length); + Assert.assertEquals(USER1 + "@email.com", addresses[0].toString()); + } + finally + { + if (groupName != null) + { + AUTHORITY_SERVICE.deleteAuthority(groupName, true); + } + PERSON_SERVICE.deletePerson(USER1); + PERSON_SERVICE.deletePerson(USER2); + } + } + + @Test + public void testPrepareEmailSubjectParams() + { + Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); + mailAction.setParameterValue(MailActionExecuter.PARAM_TO, "some.bodyelse@example.com"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, "alfresco/templates/mail/test.txt.ftl"); + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Test Subject Params"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, getModel()); + Pair recipient = new Pair<>("test", Locale.ENGLISH); + + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT_PARAMS, new Object[] {"Test", "Subject", "Params", "Object", "Array"}); + Assert.assertNotNull("We should support Object[] value for PARAM_SUBJECT_PARAMS", ACTION_EXECUTER.prepareEmail(mailAction, null, recipient, null)); + + ArrayList params = new ArrayList<>(); + params.add("Test"); + params.add("Subject"); + params.add("Params"); + params.add("ArrayList"); + + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT_PARAMS, params); + Assert.assertNotNull("We should support List value for PARAM_SUBJECT_PARAMS", ACTION_EXECUTER.prepareEmail(mailAction, null, recipient, null)); + + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT_PARAMS, "Test Subject Params Single String"); + Assert.assertNotNull("We should support String value for PARAM_SUBJECT_PARAMS", ACTION_EXECUTER.prepareEmail(mailAction, null, recipient, null)); + + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT_PARAMS, null); + Assert.assertNotNull("We should support null value for PARAM_SUBJECT_PARAMS", ACTION_EXECUTER.prepareEmail(mailAction, null, recipient, null)); + } + + /** + * Creates a test user with the specified username and optionally custom email. + * + * @param userName String + * @param email Optional, if not specified assigned to userName + "@email.com" + * @return NodeRef + */ + private NodeRef createUser(String userName, String email) + { + PropertyMap personProps = new PropertyMap(); + personProps.put(ContentModel.PROP_USERNAME, userName); + personProps.put(ContentModel.PROP_FIRSTNAME, userName); + personProps.put(ContentModel.PROP_LASTNAME, userName); + personProps.put(ContentModel.PROP_EMAIL, Objects.requireNonNullElseGet(email, () -> userName + "@email.com")); + + return PERSON_SERVICE.createPerson(personProps); + } + + /** + * Test for MNT-10874 + */ + @Test + public void testUserWithNonExistingTenant() + { + final String USER_WITH_NON_EXISTING_TENANT = "test_user_non_tenant@non_existing_tenant.com"; + + try + { + createUser(USER_WITH_NON_EXISTING_TENANT, USER_WITH_NON_EXISTING_TENANT); + + final Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); + mailAction.setParameterValue(MailActionExecuter.PARAM_TO, USER_WITH_NON_EXISTING_TENANT); + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TEXT, "This is a test message."); + + // run as non admin and non system + AuthenticationUtil.setFullyAuthenticatedUser(BRITISH_USER.getUsername()); + TRANSACTION_SERVICE.getRetryingTransactionHelper().doInTransaction( + (RetryingTransactionCallback) () -> { + ACTION_EXECUTER.executeImpl(mailAction, null); + return null; + }); + } + finally + { + // restore system user as current user + AuthenticationUtil.setRunAsUserSystem(); + // tidy up + PERSON_SERVICE.deletePerson(USER_WITH_NON_EXISTING_TENANT); + AuthenticationUtil.clearCurrentSecurityContext(); + } + } + + /** + * Test for MNT-11488 + * @throws MessagingException + */ + @Test + public void testSendingToMultipleUsers() throws MessagingException + { + final String USER_1 = "recipient1"; + final String USER_2 = "recipient2"; + final String[] recipientsArray = { USER_1 + "@email.com", USER_2 + "@email.com" }; + final List recipientsResult = new ArrayList<>(Arrays.asList(recipientsArray)) ; + + try + { + createUser(USER_1, null); + createUser(USER_2, null); + ArrayList recipients = new ArrayList<>(2); + recipients.add(USER_1); + recipients.add(USER_2); + + Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); + mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "sender@example.com"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TO_MANY, recipients); + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, getModel()); + + ACTION_EXECUTER.resetTestSentCount(); + + ACTION_SERVICE.executeAction(mailAction, null); + + MimeMessage message = ACTION_EXECUTER.retrieveLastTestMessage(); + Assert.assertNotNull(message); + Assert.assertEquals("One email should be sent", 1, ACTION_EXECUTER.getTestSentCount()); + Assert.assertEquals("All recipients should receive single message", 2, message.getAllRecipients().length); + + Assert.assertTrue("Both users should receive message", recipientsResult.contains(((InternetAddress) message.getAllRecipients()[0]).getAddress())); + Assert.assertTrue("Both users should receive message", recipientsResult.contains(((InternetAddress) message.getAllRecipients()[1]).getAddress())); + } + finally + { + // tidy up + PERSON_SERVICE.deletePerson(USER_1); + PERSON_SERVICE.deletePerson(USER_2); + } + } + + /** + * Test for CC / BCC + * @throws MessagingException + */ + @Test + public void testSendingToCarbonCopy() throws MessagingException + { + // PARAM_TO variant + Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); + mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TO, "some.bodyelse@example.com"); + mailAction.setParameterValue(MailActionExecuter.PARAM_CC, "some.carbon@example.com"); + mailAction.setParameterValue(MailActionExecuter.PARAM_BCC, "some.blindcarbon@example.com"); + + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing CARBON COPY"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, "alfresco/templates/mail/test.txt.ftl"); + + mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, getModel()); + + ACTION_SERVICE.executeAction(mailAction, null); + + MimeMessage message = ACTION_EXECUTER.retrieveLastTestMessage(); + Assert.assertNotNull(message); + Address[] all = message.getAllRecipients(); + Address[] ccs = message.getRecipients(RecipientType.CC); + Address[] bccs = message.getRecipients(RecipientType.BCC); + Assert.assertEquals(3, all.length); + Assert.assertEquals(1, ccs.length); + Assert.assertEquals(1, bccs.length); + Assert.assertTrue(ccs[0].toString().contains("some.carbon")); + Assert.assertTrue(bccs[0].toString().contains("some.blindcarbon")); + } + + /** + * Test for MNT-11079 + */ + @Test + public void testSendingToUserWithMailAlikeName() throws IOException, MessagingException + { + final String USER_1 = "user1@namelookslikeemail"; + final String USER_1_EMAIL = "user1@trueemail.com"; + + try + { + createUser(USER_1, USER_1_EMAIL); + + Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); + mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TO_MANY, USER_1); + + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, "alfresco/templates/mail/test.txt.ftl"); + + mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, getModel()); + + ACTION_SERVICE.executeAction(mailAction, null); + + MimeMessage message = ACTION_EXECUTER.retrieveLastTestMessage(); + Assert.assertNotNull(message); + Assert.assertEquals("Hello Jan 1, 1970", message.getContent()); + Assert.assertEquals(1, message.getAllRecipients().length); + javax.mail.internet.InternetAddress address = (InternetAddress) message.getAllRecipients()[0]; + Assert.assertEquals(USER_1_EMAIL, address.getAddress()); + } + finally + { + // tidy up + PERSON_SERVICE.deletePerson(USER_1); + } + } + + /** + * Test for MNT-12464 + */ + @Test + public void testMultipleIdenticalEmailsToUser() + { + final String USER_1 = "user12464_1"; + final String USER_2 = "user12464_2"; + final String USER_3 = "user12464_3"; + final String USER_6 = "user12464_6"; + + final String USER_4_USERNAME = "user12464_4@mnt12464mail.com"; + final String USER_5_USERNAME = "user12464_5@mnt12464mail.com"; + + String[] users = new String[] { USER_1, USER_2, USER_3, USER_4_USERNAME, USER_5_USERNAME }; + + final String GROUP_1_SHORT_NAME = "mnt12464group1"; + final String GROUP_1 = "GROUP_" + GROUP_1_SHORT_NAME; + final String GROUP_2_SHORT_NAME = "mnt12464group2"; + final String GROUP_2 = "GROUP_" + GROUP_2_SHORT_NAME; + + try + { + createUser(USER_1, null); + createUser(USER_2, null); + createUser(USER_3, null); + AUTHORITY_SERVICE.createAuthority(AuthorityType.GROUP, GROUP_1_SHORT_NAME); + AUTHORITY_SERVICE.createAuthority(AuthorityType.GROUP, GROUP_2_SHORT_NAME); + AUTHORITY_SERVICE.addAuthority(GROUP_1, USER_1); + AUTHORITY_SERVICE.addAuthority(GROUP_1, USER_2); + AUTHORITY_SERVICE.addAuthority(GROUP_2, USER_1); + AUTHORITY_SERVICE.addAuthority(GROUP_2, USER_2); + AUTHORITY_SERVICE.addAuthority(GROUP_2, USER_3); + + // these persons should be without emails + PropertyMap personProps = new PropertyMap(); + personProps.put(ContentModel.PROP_USERNAME, USER_4_USERNAME); + personProps.put(ContentModel.PROP_FIRSTNAME, USER_4_USERNAME); + personProps.put(ContentModel.PROP_LASTNAME, USER_4_USERNAME); + PERSON_SERVICE.createPerson(personProps); + AUTHORITY_SERVICE.addAuthority(GROUP_1, USER_4_USERNAME); + AUTHORITY_SERVICE.addAuthority(GROUP_2, USER_4_USERNAME); + + personProps = new PropertyMap(); + personProps.put(ContentModel.PROP_USERNAME, USER_5_USERNAME); + personProps.put(ContentModel.PROP_FIRSTNAME, USER_5_USERNAME); + personProps.put(ContentModel.PROP_LASTNAME, USER_5_USERNAME); + PERSON_SERVICE.createPerson(personProps); + + Action mailAction1 = ACTION_SERVICE.createAction(MailActionExecuter.NAME); + mailAction1.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); + ArrayList toMany1 = new ArrayList<>(); + toMany1.add(USER_1); + toMany1.add(GROUP_1); + toMany1.add(USER_2); + toMany1.add(GROUP_2); + toMany1.add(USER_3); + toMany1.add(USER_4_USERNAME); + toMany1.add(USER_5_USERNAME); + mailAction1.setParameterValue(MailActionExecuter.PARAM_TO_MANY, toMany1); + mailAction1.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing MNT-12464"); + mailAction1.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, "alfresco/templates/mail/test.txt.ftl"); + mailAction1.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, getModel()); + + ACTION_EXECUTER.resetTestSentCount(); + ACTION_SERVICE.executeAction(mailAction1, null); + + Assert.assertEquals("Must be received one letter on each recipient", users.length, ACTION_EXECUTER.getTestSentCount()); + + // testing for GROUP_EVERYONE + + ACTION_EXECUTER.resetTestSentCount(); + everyoneSending(); + int before = ACTION_EXECUTER.getTestSentCount(); + // create one additional user + NodeRef user6 = createUser(USER_6, null); + PERMISSION_SERVICE.setInheritParentPermissions(user6, false); + PERMISSION_SERVICE.deletePermissions(user6); + + // USER_6 not exist for USER_1, but he will be added to recipients + int after = AuthenticationUtil.runAs(() -> { + ACTION_EXECUTER.resetTestSentCount(); + everyoneSending(); + return ACTION_EXECUTER.getTestSentCount(); + }, USER_1); + + Assert.assertEquals("One additional user was created, quantity of recipients GROUP_EVERYONE must be +1 user", 1, after - before); + } + finally + { + PERSON_SERVICE.deletePerson(USER_1); + PERSON_SERVICE.deletePerson(USER_2); + PERSON_SERVICE.deletePerson(USER_3); + PERSON_SERVICE.deletePerson(USER_4_USERNAME); + PERSON_SERVICE.deletePerson(USER_5_USERNAME); + PERSON_SERVICE.deletePerson(USER_6); + AUTHORITY_SERVICE.deleteAuthority(GROUP_1); + AUTHORITY_SERVICE.deleteAuthority(GROUP_2); + } + } + + private void everyoneSending() + { + Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); + mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); + ArrayList toMany = new ArrayList<>(); + toMany.add(PERMISSION_SERVICE.getAllAuthorities()); + mailAction.setParameterValue(MailActionExecuter.PARAM_TO_MANY, toMany); + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing MNT-12464"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, "alfresco/templates/mail/test.txt.ftl"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, getModel()); + + ACTION_EXECUTER.resetTestSentCount(); + + ACTION_SERVICE.executeAction(mailAction, null); + } + + /** + * ACE-2564 + */ + @Test + public void testSendEmailByExternalUser() throws IOException, MessagingException + { + final Serializable recipients = (Serializable) Arrays.asList(BRITISH_USER.getUsername()); + final String subject = ""; + final String template = "alfresco/templates/mail/test.txt.ftl"; + AuthenticationUtil.pushAuthentication(); + AuthenticationUtil.setFullyAuthenticatedUser(EXTERNAL_USER.getUsername()); + + try + { + // these people should be without emails + // testing for GROUP_EVERYONE + final String tenantId = getUsersHomeTenant(BRITISH_USER.getUsername()); + + // USER_6 not exist for USER_1, but he will be added to recipients + MimeMessage message = TenantUtil.runAsTenant(() -> sendMessage(null, recipients, subject, template), tenantId); + + Assert.assertNotNull(message); + Assert.assertEquals("Hello 1 Jan 1970", message.getContent()); + } + finally + { + AuthenticationUtil.popAuthentication(); + } + } + + /** + * Test for MNT-17970 + * @throws IOException + * @throws MessagingException + */ + @Test + public void testGetToUsersWhenSendingToGroup() throws IOException, MessagingException + { + String groupName = null; + final String USER1 = "test_user1"; + final String USER2 = "test_user2"; + try + { + // Create users and add them to a group + createUser(USER1, null); + createUser(USER2, null); + groupName = AUTHORITY_SERVICE.createAuthority(AuthorityType.GROUP, "testgroup1"); + AUTHORITY_SERVICE.addAuthority(groupName, USER1); + AUTHORITY_SERVICE.addAuthority(groupName, USER2); + + // Create mail + final Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); + mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TO_MANY, groupName); + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TEXT, "Testing"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, "alfresco/templates/mail/testSentTo.txt.ftl"); + + RetryingTransactionHelper txHelper = APP_CONTEXT_INIT.getApplicationContext().getBean("retryingTransactionHelper", RetryingTransactionHelper.class); + + // Send mail + MimeMessage message = txHelper.doInTransaction(() -> { + ACTION_EXECUTER.executeImpl(mailAction, null); + return ACTION_EXECUTER.retrieveLastTestMessage(); + }, true); + + // Check that both users are displayed in message body + String recipients = USER1 + "@email.com" + "," + USER2 + "@email.com"; + Assert.assertNotNull(message); + Assert.assertEquals("This email was sent to " + recipients, message.getContent()); + } + finally + { + if (groupName != null) + { + AUTHORITY_SERVICE.deleteAuthority(groupName, true); + } + PERSON_SERVICE.deletePerson(USER1); + PERSON_SERVICE.deletePerson(USER2); + } + } + + /** + * ALF-21948 + */ + @Test + public void testSendingToArrayOfCarbonCopyAndBlindCarbonCopyUsers() throws MessagingException + { + Map params = new HashMap<>(); + String[] ccArray = { "cc_user1@example.com", "cc_user2@example.com" }; + String[] bccArray = { "bcc_user3@example.com", "bcc_user4@example.com", "bcc_user5@example.com" }; + params.put(MailActionExecuter.PARAM_FROM, "sender@email.com"); + params.put(MailActionExecuter.PARAM_TO, "test@email.com"); + params.put(MailActionExecuter.PARAM_CC, ccArray); + params.put(MailActionExecuter.PARAM_BCC, bccArray); + + params.put(MailActionExecuter.PARAM_TEXT, "Mail body here"); + params.put(MailActionExecuter.PARAM_SUBJECT, "Subject text"); + + Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME, params); + ACTION_EXECUTER.resetTestSentCount(); + + ACTION_SERVICE.executeAction(mailAction, null); + MimeMessage message = ACTION_EXECUTER.retrieveLastTestMessage(); + Assert.assertNotNull(message); + + Address[] all = message.getAllRecipients(); + Address[] ccs = message.getRecipients(RecipientType.CC); + Address[] bccs = message.getRecipients(RecipientType.BCC); + Assert.assertEquals(6, all.length); + Assert.assertEquals(2, ccs.length); + Assert.assertEquals(3, bccs.length); + Assert.assertTrue(ccs[0].toString().contains("cc_user1") && ccs[1].toString().contains("cc_user2")); + Assert.assertTrue(bccs[0].toString().contains("bcc_user3") && bccs[1].toString().contains("bcc_user4") + && bccs[2].toString().contains("bcc_user5")); + } + + /** + * ALF-21948 + */ + @Test + public void testSendingToListOfCarbonCopyAndBlindCarbonCopyUsers() throws MessagingException + { + List ccList = new ArrayList<>(); + ccList.add("cc_user1@example.com"); + ccList.add("cc_user2@example.com"); + + List bccList = new ArrayList<>(); + bccList.add("bcc_user3@example.com"); + bccList.add("bcc_user4@example.com"); + bccList.add("bcc_user5@example.com"); + + Action mailAction = ACTION_SERVICE.createAction(MailActionExecuter.NAME); + mailAction.setParameterValue(MailActionExecuter.PARAM_FROM, "some.body@example.com"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TO, "some.bodyelse@example.com"); + mailAction.setParameterValue(MailActionExecuter.PARAM_CC, (Serializable) ccList); + mailAction.setParameterValue(MailActionExecuter.PARAM_BCC, (Serializable) bccList); + + mailAction.setParameterValue(MailActionExecuter.PARAM_SUBJECT, "Testing (BLIND) CARBON COPY"); + mailAction.setParameterValue(MailActionExecuter.PARAM_TEXT, "mail body here"); + + ACTION_EXECUTER.resetTestSentCount(); + ACTION_SERVICE.executeAction(mailAction, null); + MimeMessage message = ACTION_EXECUTER.retrieveLastTestMessage(); + Assert.assertNotNull(message); + + Address[] all = message.getAllRecipients(); + Address[] ccs = message.getRecipients(RecipientType.CC); + Address[] bccs = message.getRecipients(RecipientType.BCC); + Assert.assertEquals(6, all.length); + Assert.assertEquals(2, ccs.length); + Assert.assertEquals(3, bccs.length); + Assert.assertTrue(ccs[0].toString().contains("cc_user1") && ccs[1].toString().contains("cc_user2")); + Assert.assertTrue(bccs[0].toString().contains("bcc_user3") && bccs[1].toString().contains("bcc_user4") + && bccs[2].toString().contains("bcc_user5")); + } + +} diff --git a/repository/src/test/java/org/alfresco/repo/action/executer/AsynchronousExtractorTest.java b/repository/src/test/java/org/alfresco/repo/action/executer/AsynchronousExtractorTest.java index 1f9db9647d..674ca7ae61 100644 --- a/repository/src/test/java/org/alfresco/repo/action/executer/AsynchronousExtractorTest.java +++ b/repository/src/test/java/org/alfresco/repo/action/executer/AsynchronousExtractorTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2021 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -58,7 +58,7 @@ import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; import org.alfresco.test_category.BaseSpringTestsCategory; -import org.alfresco.transform.client.registry.TransformServiceRegistry; +import org.alfresco.transform.registry.TransformServiceRegistry; import org.alfresco.util.BaseSpringTest; import org.alfresco.util.GUID; import org.junit.After; diff --git a/repository/src/test/java/org/alfresco/repo/content/transform/LocalTransformServiceRegistryTest.java b/repository/src/test/java/org/alfresco/repo/content/transform/LocalTransformServiceRegistryTest.java index 43947bce6a..57902596c3 100644 --- a/repository/src/test/java/org/alfresco/repo/content/transform/LocalTransformServiceRegistryTest.java +++ b/repository/src/test/java/org/alfresco/repo/content/transform/LocalTransformServiceRegistryTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2021 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -25,44 +25,39 @@ */ package org.alfresco.repo.content.transform; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.InjectMocks; -import org.mockito.Spy; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -import java.io.IOException; -import java.util.Properties; -import java.util.StringJoiner; - import static org.alfresco.repo.content.transform.LocalTransformServiceRegistry.LOCAL_TRANSFORMER; import static org.alfresco.repo.content.transform.LocalTransformServiceRegistry.URL; import static org.junit.Assert.assertEquals; +import java.util.Properties; +import java.util.StringJoiner; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) public class LocalTransformServiceRegistryTest { @Spy - private Properties properties = new Properties(); - + private Properties properties; @InjectMocks - LocalTransformServiceRegistry registry = new LocalTransformServiceRegistry(); - - @Rule - public MockitoRule initRule = MockitoJUnit.rule(); + private LocalTransformServiceRegistry registry; @Test - public void testGetTEngineUrlsSortedByName() throws IOException + public void testGetTEngineUrlsSortedByName() { - properties.put(LOCAL_TRANSFORMER+"aa"+URL, "aa"); - properties.put(LOCAL_TRANSFORMER+"engine1"+URL, "http_xxxx1"); - properties.put(LOCAL_TRANSFORMER+"engine3"+URL, "http3"); - properties.put(LOCAL_TRANSFORMER+"engine2"+URL, "http_xx2"); - properties.put(LOCAL_TRANSFORMER+"bb"+URL, "bb"); - properties.put(LOCAL_TRANSFORMER+"b"+URL, "b"); + properties.put(LOCAL_TRANSFORMER + "aa" + URL, "aa"); + properties.put(LOCAL_TRANSFORMER + "engine1" + URL, "http_xxxx1"); + properties.put(LOCAL_TRANSFORMER + "engine3" + URL, "http3"); + properties.put(LOCAL_TRANSFORMER + "engine2" + URL, "http_xx2"); + properties.put(LOCAL_TRANSFORMER + "bb" + URL, "bb"); + properties.put(LOCAL_TRANSFORMER + "b" + URL, "b"); StringJoiner orderEngineConfigRead = new StringJoiner(","); - registry.getTEngineUrlsSortedByName().forEach(name -> orderEngineConfigRead.add(name)); + registry.getTEngineUrlsSortedByName().forEach(orderEngineConfigRead::add); assertEquals("aa,b,bb,http_xxxx1,http_xx2,http3", orderEngineConfigRead.toString()); } } diff --git a/repository/src/test/java/org/alfresco/repo/copy/CopyServiceImplTest.java b/repository/src/test/java/org/alfresco/repo/copy/CopyServiceImplTest.java index 3166d3d920..90c5874ad0 100644 --- a/repository/src/test/java/org/alfresco/repo/copy/CopyServiceImplTest.java +++ b/repository/src/test/java/org/alfresco/repo/copy/CopyServiceImplTest.java @@ -737,6 +737,7 @@ public class CopyServiceImplTest extends TestCase // Create a new rule and add it to the source noderef Rule rule = new Rule(); rule.setRuleType(RuleType.INBOUND); + rule.setTitle("Rule name"); Map props = new HashMap(1); props.put(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_VERSIONABLE); @@ -1010,7 +1011,8 @@ public class CopyServiceImplTest extends TestCase Map params = new HashMap(1); params.put(MoveActionExecuter.PARAM_DESTINATION_FOLDER, nodeTwo); Rule rule = new Rule(); - rule.setRuleType(RuleType.INBOUND); + rule.setRuleType(RuleType.INBOUND); + rule.setTitle("Rule name"); Action action = actionService.createAction(CopyActionExecuter.NAME, params); ActionCondition condition = actionService.createActionCondition(NoConditionEvaluator.NAME); action.addActionCondition(condition); diff --git a/repository/src/test/java/org/alfresco/repo/domain/schema/script/DeleteNotExistsV3ExecutorTest.java b/repository/src/test/java/org/alfresco/repo/domain/schema/script/DeleteNotExistsV3ExecutorTest.java new file mode 100644 index 0000000000..af6e91e0da --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/domain/schema/script/DeleteNotExistsV3ExecutorTest.java @@ -0,0 +1,201 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.domain.schema.script; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.sql.Connection; +import java.util.List; +import java.util.Properties; + +import javax.sql.DataSource; + +import org.alfresco.repo.domain.dialect.Dialect; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.testing.category.DBTests; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.mockito.Mockito; +import org.springframework.context.ApplicationContext; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Integration tests for the {@link DeleteNotExistsV3Executor} class. + * + * @author Eva Vasques + */ +@Category({DBTests.class}) +public class DeleteNotExistsV3ExecutorTest +{ + private static ApplicationContext ctx; + private ScriptExecutor scriptExecutor; + private DataSource dataSource; + private Dialect dialect; + private JdbcTemplate jdbcTmpl; + + @BeforeClass + public static void setUpBeforeClass() + { + String[] config = new String[] { "classpath:alfresco/application-context.xml", "classpath:scriptexec/script-exec-test.xml" }; + ctx = ApplicationContextHelper.getApplicationContext(config); + } + + @Before + public void setUp() throws Exception + { + scriptExecutor = ctx.getBean("simpleScriptExecutor", ScriptExecutorImpl.class); + dataSource = ctx.getBean("dataSource", DataSource.class); + dialect = ctx.getBean("dialect", Dialect.class); + jdbcTmpl = new JdbcTemplate(dataSource); + } + + private DeleteNotExistsV3Executor createDeleteNotExistsV3Executor(Dialect dialect, Connection connection, String sql, int line, File scriptFile, Properties properties) + { + return new DeleteNotExistsV3Executor(dialect, connection, sql, line, scriptFile, properties, dataSource); + } + + @Test() + public void testDefaultBehaviour() throws Exception + { + scriptExecutor.executeScriptUrl("scriptexec/${db.script.dialect}/delete-not-exists/test-data1.sql"); + + String sql = "--DELETE_NOT_EXISTS_V3 temp_tst_tbl_1.id,temp_tst_tbl_2.tbl_2_id,temp_tst_tbl_3.tbl_3_id,temp_tst_tbl_4.tbl_4_id system.delete_not_exists.batchsize"; + int line = 1; + File scriptFile = Mockito.mock(File.class); + Properties properties = Mockito.mock(Properties.class); + + String select = "select id from temp_tst_tbl_1 order by id ASC"; + + try (Connection connection = dataSource.getConnection()) + { + connection.setAutoCommit(true); + + // Test read only + { + when(properties.getProperty(DeleteNotExistsV3Executor.PROPERTY_READ_ONLY)).thenReturn("true"); + when(properties.getProperty(DeleteNotExistsV3Executor.PROPERTY_TIMEOUT_SECONDS)).thenReturn("-1"); + DeleteNotExistsV3Executor DeleteNotExistsV3Executor = createDeleteNotExistsV3Executor(dialect, connection, sql, line, scriptFile, properties); + DeleteNotExistsV3Executor.execute(); + + List res = jdbcTmpl.queryForList(select, String.class); + assertEquals(7, res.size()); + } + } + + try (Connection connection = dataSource.getConnection()) + { + connection.setAutoCommit(true); + + // Test with delete + { + when(properties.getProperty(DeleteNotExistsV3Executor.PROPERTY_READ_ONLY)).thenReturn("false"); + when(properties.getProperty(DeleteNotExistsV3Executor.PROPERTY_TIMEOUT_SECONDS)).thenReturn("-1"); + DeleteNotExistsV3Executor DeleteNotExistsV3Executor = createDeleteNotExistsV3Executor(dialect, connection, sql, line, scriptFile, properties); + DeleteNotExistsV3Executor.execute(); + + List res = jdbcTmpl.queryForList(select, String.class); + assertEquals(5, res.size()); + + assertEquals("1", res.get(0)); + assertEquals("2", res.get(1)); + assertEquals("4", res.get(2)); + assertEquals("10", res.get(3)); + assertEquals("11", res.get(4)); + } + } + } + + @Test() + public void testDeleteBatch() throws Exception + { + scriptExecutor.executeScriptUrl("scriptexec/${db.script.dialect}/delete-not-exists/test-data1.sql"); + + String sql = "--DELETE_NOT_EXISTS_V3 temp_tst_tbl_1.id,temp_tst_tbl_2.tbl_2_id,temp_tst_tbl_3.tbl_3_id,temp_tst_tbl_4.tbl_4_id system.delete_not_exists.batchsize"; + int line = 1; + File scriptFile = Mockito.mock(File.class); + Properties properties = Mockito.mock(Properties.class); + + String select = "select id from temp_tst_tbl_1 order by id ASC"; + + try (Connection connection = dataSource.getConnection()) + { + connection.setAutoCommit(true); + { + when(properties.getProperty(DeleteNotExistsV3Executor.PROPERTY_DELETE_BATCH_SIZE)).thenReturn("1"); + when(properties.getProperty(DeleteNotExistsV3Executor.PROPERTY_READ_ONLY)).thenReturn("false"); + DeleteNotExistsV3Executor DeleteNotExistsV3Executor = createDeleteNotExistsV3Executor(dialect, connection, sql, line, scriptFile, properties); + DeleteNotExistsV3Executor.execute(); + + List res = jdbcTmpl.queryForList(select, String.class); + assertEquals(5, res.size()); + + assertEquals("1", res.get(0)); + assertEquals("2", res.get(1)); + assertEquals("4", res.get(2)); + assertEquals("10", res.get(3)); + assertEquals("11", res.get(4)); + } + } + } + + @Test() + public void testBatchExecute() throws Exception + { + scriptExecutor.executeScriptUrl("scriptexec/${db.script.dialect}/delete-not-exists/test-data1.sql"); + + String sql = "--DELETE_NOT_EXISTS_V3 temp_tst_tbl_1.id,temp_tst_tbl_2.tbl_2_id,temp_tst_tbl_3.tbl_3_id,temp_tst_tbl_4.tbl_4_id system.delete_not_exists.batchsize"; + int line = 1; + File scriptFile = Mockito.mock(File.class); + Properties properties = Mockito.mock(Properties.class); + + String select = "select id from temp_tst_tbl_1 order by id ASC"; + + try (Connection connection = dataSource.getConnection()) + { + connection.setAutoCommit(true); + { + when(properties.getProperty(DeleteNotExistsV3Executor.PROPERTY_BATCH_SIZE)).thenReturn("2"); + when(properties.getProperty(DeleteNotExistsV3Executor.PROPERTY_READ_ONLY)).thenReturn("false"); + when(properties.getProperty(DeleteNotExistsV3Executor.PROPERTY_TIMEOUT_SECONDS)).thenReturn("-1"); + DeleteNotExistsV3Executor DeleteNotExistsV3Executor = createDeleteNotExistsV3Executor(dialect, connection, sql, line, scriptFile, properties); + DeleteNotExistsV3Executor.execute(); + + List res = jdbcTmpl.queryForList(select, String.class); + assertEquals(5, res.size()); + + assertEquals("1", res.get(0)); + assertEquals("2", res.get(1)); + assertEquals("4", res.get(2)); + assertEquals("10", res.get(3)); + assertEquals("11", res.get(4)); + } + } + } +} \ No newline at end of file diff --git a/repository/src/test/java/org/alfresco/repo/download/DownloadServiceIntegrationTest.java b/repository/src/test/java/org/alfresco/repo/download/DownloadServiceIntegrationTest.java index 3a6db742f6..ecd70f275a 100644 --- a/repository/src/test/java/org/alfresco/repo/download/DownloadServiceIntegrationTest.java +++ b/repository/src/test/java/org/alfresco/repo/download/DownloadServiceIntegrationTest.java @@ -27,6 +27,7 @@ package org.alfresco.repo.download; import net.sf.acegisecurity.Authentication; import org.alfresco.model.ContentModel; +import org.alfresco.repo.action.executer.AddFeaturesActionExecuter; import org.alfresco.repo.model.Repository; import org.alfresco.repo.node.SystemNodeUtils; import org.alfresco.repo.node.integrity.IntegrityChecker; @@ -34,6 +35,8 @@ import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.permissions.AccessDeniedException; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionService; import org.alfresco.service.cmr.admin.RepoAdminService; import org.alfresco.service.cmr.coci.CheckOutCheckInService; import org.alfresco.service.cmr.download.DownloadService; @@ -47,6 +50,8 @@ import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.cmr.rule.RuleType; import org.alfresco.service.cmr.security.MutableAuthenticationService; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.security.PersonService; @@ -75,6 +80,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.io.UnsupportedEncodingException; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; @@ -82,6 +88,7 @@ import java.util.Set; import java.util.TreeSet; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** @@ -128,6 +135,8 @@ public class DownloadServiceIntegrationTest private static RetryingTransactionHelper TRANSACTION_HELPER; private static IntegrityChecker INTEGRITY_CHECKER; private static RepoAdminService REPO_ADMIN_SERVICE; + private static RuleService RULE_SERVICE; + private static ActionService ACTION_SERVICE; // Test Content private NodeRef rootFolder; @@ -192,6 +201,8 @@ public class DownloadServiceIntegrationTest INTEGRITY_CHECKER.setFailOnViolation(true); INTEGRITY_CHECKER.setTraceOn(true); REPO_ADMIN_SERVICE = APP_CONTEXT_INIT.getApplicationContext().getBean("RepoAdminService", RepoAdminService.class); + RULE_SERVICE = APP_CONTEXT_INIT.getApplicationContext().getBean("ruleService", RuleService.class); + ACTION_SERVICE = APP_CONTEXT_INIT.getApplicationContext().getBean("actionService", ActionService.class); } /** @@ -207,28 +218,49 @@ public class DownloadServiceIntegrationTest NodeRef COMPANY_HOME = repositoryHelper.getCompanyHome(); // Create some static test content - rootFolder = testNodes.createNode(COMPANY_HOME, "rootFolder", ContentModel.TYPE_FOLDER, AuthenticationUtil.getAdminUserName()); - allEntries.add("rootFolder/"); + rootFolder = testNodes.createNode(COMPANY_HOME, "rootFolder", ContentModel.TYPE_FOLDER, AuthenticationUtil.getAdminUserName()); + allEntries.add("rootFolder/"); + + TRANSACTION_HELPER.doInTransaction(() -> { + org.alfresco.service.cmr.rule.Rule parentRule = new org.alfresco.service.cmr.rule.Rule(); + parentRule.setRuleTypes(Collections.singletonList(RuleType.INBOUND)); + parentRule.setTitle("DownloadServiceIntegrationTest" + GUID.generate()); + parentRule.setDescription("Add Classifiable"); + Action action = ACTION_SERVICE.createAction(AddFeaturesActionExecuter.NAME); + action.setParameterValue(AddFeaturesActionExecuter.PARAM_ASPECT_NAME, ContentModel.ASPECT_CLASSIFIABLE); + parentRule.setAction(action); + parentRule.applyToChildren(true); + + RULE_SERVICE.saveRule(rootFolder, parentRule); + + return null; + }, false, true); rootFile = testNodes.createNodeWithTextContent(COMPANY_HOME, "rootFile.txt", ContentModel.TYPE_CONTENT, AuthenticationUtil.getAdminUserName(), "Root file content"); allEntries.add("rootFile.txt"); - - testNodes.createNodeWithTextContent(rootFolder, "level1File.txt", ContentModel.TYPE_CONTENT, AuthenticationUtil.getAdminUserName(), "Level 1 file content"); + + NodeRef createdNode = testNodes.createNodeWithTextContent(rootFolder, "level1File.txt", ContentModel.TYPE_CONTENT, AuthenticationUtil.getAdminUserName(), "Level 1 file content"); + assertTrue(NODE_SERVICE.hasAspect(createdNode, ContentModel.ASPECT_CLASSIFIABLE)); allEntries.add("rootFolder/level1File.txt"); level1Folder1 = testNodes.createNode(rootFolder, "level1Folder1", ContentModel.TYPE_FOLDER, AuthenticationUtil.getAdminUserName()); + assertTrue(NODE_SERVICE.hasAspect(level1Folder1, ContentModel.ASPECT_CLASSIFIABLE)); allEntries.add("rootFolder/level1Folder1/"); level1Folder2 = testNodes.createNode(rootFolder, "level1Folder2", ContentModel.TYPE_FOLDER, AuthenticationUtil.getAdminUserName()); + assertTrue(NODE_SERVICE.hasAspect(level1Folder2, ContentModel.ASPECT_CLASSIFIABLE)); allEntries.add("rootFolder/level1Folder2/"); - testNodes.createNode(rootFolder, "level1EmptyFolder", ContentModel.TYPE_FOLDER, AuthenticationUtil.getAdminUserName()); + createdNode = testNodes.createNode(rootFolder, "level1EmptyFolder", ContentModel.TYPE_FOLDER, AuthenticationUtil.getAdminUserName()); + assertTrue(NODE_SERVICE.hasAspect(createdNode, ContentModel.ASPECT_CLASSIFIABLE)); allEntries.add("rootFolder/level1EmptyFolder/"); - testNodes.createNodeWithTextContent(level1Folder1, "level2File.txt", ContentModel.TYPE_CONTENT, AuthenticationUtil.getAdminUserName(), "Level 2 file content"); + createdNode = testNodes.createNodeWithTextContent(level1Folder1, "level2File.txt", ContentModel.TYPE_CONTENT, AuthenticationUtil.getAdminUserName(), "Level 2 file content"); + assertTrue(NODE_SERVICE.hasAspect(createdNode, ContentModel.ASPECT_CLASSIFIABLE)); allEntries.add("rootFolder/level1Folder1/level2File.txt"); - testNodes.createNodeWithTextContent(level1Folder2, "level2File.txt", ContentModel.TYPE_CONTENT, AuthenticationUtil.getAdminUserName(), "Level 2 file content"); + createdNode = testNodes.createNodeWithTextContent(level1Folder2, "level2File.txt", ContentModel.TYPE_CONTENT, AuthenticationUtil.getAdminUserName(), "Level 2 file content"); + assertTrue(NODE_SERVICE.hasAspect(createdNode, ContentModel.ASPECT_CLASSIFIABLE)); allEntries.add("rootFolder/level1Folder2/level2File.txt"); secondaryNode = testNodes.createNodeWithTextContent(COMPANY_HOME, "secondaryNodeFile.txt", ContentModel.TYPE_CONTENT, AuthenticationUtil.getAdminUserName(), "Secondary node"); @@ -240,6 +272,7 @@ public class DownloadServiceIntegrationTest // Add the lock and version aspects to the created node NODE_SERVICE.addAspect(fileToCheckout, ContentModel.ASPECT_VERSIONABLE, null); NODE_SERVICE.addAspect(fileToCheckout, ContentModel.ASPECT_LOCKABLE, null); + assertTrue(NODE_SERVICE.hasAspect(fileToCheckout, ContentModel.ASPECT_CLASSIFIABLE)); allEntries.add("rootFolder/level1Folder2/fileToCheckout.txt"); PERMISSION_SERVICE.setPermission(level1Folder2, TEST_USER.getUsername(), PermissionService.ALL_PERMISSIONS, true); diff --git a/repository/src/test/java/org/alfresco/repo/exporter/ExporterComponentTest.java b/repository/src/test/java/org/alfresco/repo/exporter/ExporterComponentTest.java index d3d37a8ee3..c3aadc0643 100644 --- a/repository/src/test/java/org/alfresco/repo/exporter/ExporterComponentTest.java +++ b/repository/src/test/java/org/alfresco/repo/exporter/ExporterComponentTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -33,7 +33,10 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Serializable; +import java.io.BufferedReader; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Enumeration; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -41,6 +44,8 @@ import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; import org.alfresco.model.ContentModel; import org.alfresco.repo.importer.ACPImportPackageHandler; @@ -50,12 +55,7 @@ import org.alfresco.repo.security.permissions.PermissionServiceSPI; import org.alfresco.service.ServiceRegistry; import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileInfo; -import org.alfresco.service.cmr.repository.ChildAssociationRef; -import org.alfresco.service.cmr.repository.ContentData; -import org.alfresco.service.cmr.repository.MLText; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.NodeService; -import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.repository.*; import org.alfresco.service.cmr.search.CategoryService; import org.alfresco.service.cmr.security.AccessPermission; import org.alfresco.service.cmr.security.AccessStatus; @@ -68,6 +68,7 @@ import org.alfresco.service.cmr.view.ExporterService; import org.alfresco.service.cmr.view.ImportPackageHandler; import org.alfresco.service.cmr.view.ImporterService; import org.alfresco.service.cmr.view.Location; + import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; import org.alfresco.test_category.OwnJVMTestsCategory; @@ -82,6 +83,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.experimental.categories.Category; import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.extensions.surf.util.InputStreamContent; import org.springframework.transaction.annotation.Transactional; @Category({OwnJVMTestsCategory.class, LuceneTests.class}) @@ -95,6 +97,7 @@ public class ExporterComponentTest extends BaseSpringTest private FileFolderService fileFolderService; private CategoryService categoryService; private TransactionService transactionService; + private ContentService contentService; private StoreRef storeRef; private AuthenticationComponent authenticationComponent; private PermissionServiceSPI permissionService; @@ -112,6 +115,7 @@ public class ExporterComponentTest extends BaseSpringTest categoryService = (CategoryService) applicationContext.getBean("categoryService"); transactionService = (TransactionService) applicationContext.getBean("transactionService"); permissionService = (PermissionServiceSPI) applicationContext.getBean("permissionService"); + contentService = (ContentService) applicationContext.getBean("contentService"); this.authenticationService = (MutableAuthenticationService) applicationContext.getBean("AuthenticationService"); this.authenticationComponent = (AuthenticationComponent) applicationContext.getBean("authenticationComponent"); @@ -151,9 +155,7 @@ public class ExporterComponentTest extends BaseSpringTest OutputStream output = new FileOutputStream(tempFile); ExporterCrawlerParameters parameters = new ExporterCrawlerParameters(); parameters.setExportFrom(location); -// parameters.setExcludeAspects(new QName[] { ContentModel.ASPECT_AUDITABLE }); -// parameters.setExcludeChildAssocs(new QName[] { ContentModel.ASSOC_CONTAINS }); - + File acpFile = TempFileProvider.createTempFile("alf", ACPExportPackageHandler.ACP_EXTENSION); File dataFile = new File("test"); File contentDir = new File("test"); @@ -162,6 +164,81 @@ public class ExporterComponentTest extends BaseSpringTest acpHandler.setExportAsFolders(true); exporterService.exportView(acpHandler, parameters, testProgress); output.close(); + + + } + + @Test + public void testExportWithChunkedList() + throws Exception + { + TestProgress testProgress = new TestProgress(); + Location location = new Location(storeRef); + + String testFile = "_testFile"; + int numberOfNodesToExport = 20; + // now export + location.setPath("/system"); + File tempFile = TempFileProvider.createTempFile("xmlexporttest", ".xml"); + OutputStream output = new FileOutputStream(tempFile); + ExporterCrawlerParameters parameters = new ExporterCrawlerParameters(); + parameters.setExportFrom(location); + + File acpFile = TempFileProvider.createTempFile("alf", ACPExportPackageHandler.ACP_EXTENSION); + File dataFile = new File("test"); + File contentDir = new File("test"); + ACPExportPackageHandler acpHandler = new ACPExportPackageHandler(new FileOutputStream(acpFile), dataFile, contentDir, null); + acpHandler.setNodeService(nodeService); + acpHandler.setExportAsFolders(true); + NodeRef nodeRef = (location == null) ? null : location.getNodeRef(); + if (nodeRef == null) + { + // If a specific node has not been provided, default to the root + nodeRef = nodeService.getRootNode(location.getStoreRef()); + } + NodeRef[] childRefs = new NodeRef[numberOfNodesToExport]; + + for (int i = 0; i < numberOfNodesToExport; i++) + { + Map props = new HashMap(); + props.put(ContentModel.PROP_NAME, this.getClass() + testFile + i); + childRefs[i] = nodeService.createNode(nodeRef, ContentModel.ASSOC_CONTAINS, ContentModel.ASSOC_CONTAINS, ContentModel.TYPE_CONTENT, props).getChildRef(); + } + parameters.getExportFrom().setNodeRefs(childRefs); + parameters.setCrawlSelf(true); + exporterService.setExportChunkSize("3"); + exporterService.exportView(acpHandler, parameters, testProgress); + output.close(); + ZipFile zipFile = new ZipFile(acpFile.getAbsolutePath()); + + Enumeration entries = zipFile.entries(); + int numberOfExportedNodes = 0; + while(entries.hasMoreElements()){ + ZipEntry entry = entries.nextElement(); + InputStream stream = zipFile.getInputStream(entry); + try (BufferedReader br = new BufferedReader(new InputStreamReader( + stream, StandardCharsets.UTF_8));) { + + String line; + + while ((line = br.readLine()) != null) { + + if(line.contains(testFile)){ + numberOfExportedNodes++; + } + } + } + stream.close(); + } + zipFile.close(); + + assertEquals(numberOfNodesToExport, numberOfExportedNodes); + + parameters.getExportFrom().setNodeRefs(null); + for (int i = 0; i < numberOfNodesToExport; i++) + { + nodeService.deleteNode(childRefs[i]); + } } /** diff --git a/repository/src/test/java/org/alfresco/repo/imap/ImapMessageTest.java b/repository/src/test/java/org/alfresco/repo/imap/ImapMessageTest.java index 7d1b9de47c..5431829451 100644 --- a/repository/src/test/java/org/alfresco/repo/imap/ImapMessageTest.java +++ b/repository/src/test/java/org/alfresco/repo/imap/ImapMessageTest.java @@ -455,7 +455,7 @@ public class ImapMessageTest extends TestCase }; // UID SEARCH SINCE Response[] ret = (Response[]) folder.doCommand(uid_search_since); - assertEquals("java.net.SocketException: Connection reset", ret[0].getException().toString()); + assertEquals("java.io.IOException: Connection dropped by server?", ret[0].getException().toString()); } catch (MessagingException e) { diff --git a/repository/src/test/java/org/alfresco/repo/jscript/RhinoScriptTest.java b/repository/src/test/java/org/alfresco/repo/jscript/RhinoScriptTest.java index 8d7fb6f282..d4a3ecf407 100644 --- a/repository/src/test/java/org/alfresco/repo/jscript/RhinoScriptTest.java +++ b/repository/src/test/java/org/alfresco/repo/jscript/RhinoScriptTest.java @@ -25,42 +25,39 @@ */ package org.alfresco.repo.jscript; -import static org.junit.Assert.fail; - -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import junit.framework.TestCase; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.model.ContentModel; -import org.alfresco.repo.dictionary.DictionaryComponent; -import org.alfresco.repo.dictionary.DictionaryDAO; -import org.alfresco.repo.dictionary.M2Model; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.dictionary.DictionaryComponent; +import org.alfresco.repo.dictionary.DictionaryDAO; +import org.alfresco.repo.dictionary.M2Model; import org.alfresco.repo.node.BaseNodeServiceTest; import org.alfresco.repo.security.authentication.AuthenticationComponent; -import org.alfresco.repo.security.permissions.AccessDeniedException; -import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; -import org.alfresco.service.ServiceRegistry; -import org.alfresco.service.cmr.repository.ChildAssociationRef; -import org.alfresco.service.cmr.repository.ContentService; -import org.alfresco.service.cmr.repository.ContentWriter; -import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.NodeService; -import org.alfresco.service.cmr.repository.ScriptService; -import org.alfresco.service.cmr.repository.StoreRef; -import org.alfresco.service.namespace.QName; -import org.alfresco.service.transaction.TransactionService; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.ScriptProcessor; +import org.alfresco.service.cmr.repository.ScriptService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.transaction.TransactionService; import org.alfresco.test_category.OwnJVMTestsCategory; import org.alfresco.util.ApplicationContextHelper; -import org.junit.experimental.categories.Category; -import org.mozilla.javascript.Context; -import org.mozilla.javascript.Scriptable; -import org.mozilla.javascript.ScriptableObject; +import org.junit.experimental.categories.Category; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.ScriptableObject; import org.springframework.context.ApplicationContext; + +import junit.framework.TestCase; /** @@ -434,7 +431,51 @@ public class RhinoScriptTest extends TestCase }); } - + + // MNT-21638 + public void testSecureScriptString() + { + boolean executed = executeSecureScriptString(TESTSCRIPT2, false); + assertFalse("Script shouldn't have been executed (secure = false)", executed); + + executed = executeSecureScriptString(TESTSCRIPT2, null); + assertFalse("Script shouldn't have been executed (secure = null)", executed); + + executed = executeSecureScriptString(TESTSCRIPT2, true); + assertTrue("Script should have been executed (secure = true)", executed); + } + + private boolean executeSecureScriptString(String script, Boolean secure) + { + return transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() + { + public Boolean execute() throws Exception + { + StoreRef store = nodeService.createStore(StoreRef.PROTOCOL_WORKSPACE, "rhino_" + System.currentTimeMillis()); + NodeRef root = nodeService.getRootNode(store); + BaseNodeServiceTest.buildNodeGraph(nodeService, root); + + try + { + Map model = new HashMap(); + model.put("out", System.out); + + ScriptNode rootNode = new ScriptNode(root, serviceRegistry, null); + model.put("root", rootNode); + + // test executing a script directly as string + scriptService.executeScriptString("javascript", script, model, secure); + } + catch (Exception e) + { + return false; + } + + return true; + } + }); + } + private static final String TESTSCRIPT_CLASSPATH1 = "org/alfresco/repo/jscript/test_script1.js"; private static final String TESTSCRIPT_CLASSPATH2 = "org/alfresco/repo/jscript/test_script2.js"; private static final String TESTSCRIPT_CLASSPATH3 = "org/alfresco/repo/jscript/test_script3.js"; @@ -453,7 +494,12 @@ public class RhinoScriptTest extends TestCase "logger.log(\"child by name path: \" + childByNameNode.name);\r\n" + "var xpathResults = root.childrenByXPath(\"/*\");\r\n" + "logger.log(\"children of root from xpath: \" + xpathResults.length);\r\n"; - + + private static final String TESTSCRIPT2 = "var exec = new org.alfresco.util.exec.RuntimeExec();\r\n" + + "exec.setCommand([\"/bin/ls\"]);\r\n" + + "var res = exec.execute();\r\n" + + "java.lang.System.err.println(res.getStdOut());\r\n"; + private static final String BASIC_JAVA = "var list = com.google.common.collect.Lists.newArrayList();\n" + "root.nodeRef.getClass().forName(\"java.lang.ProcessBuilder\")"; diff --git a/repository/src/test/java/org/alfresco/repo/model/ml/tools/ContentFilterLanguagesMapTest.java b/repository/src/test/java/org/alfresco/repo/model/ml/tools/ContentFilterLanguagesMapTest.java index 10a1db7f3b..42743d2649 100644 --- a/repository/src/test/java/org/alfresco/repo/model/ml/tools/ContentFilterLanguagesMapTest.java +++ b/repository/src/test/java/org/alfresco/repo/model/ml/tools/ContentFilterLanguagesMapTest.java @@ -1,28 +1,28 @@ -/* - * #%L - * Alfresco Repository - * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited - * %% - * This file is part of the Alfresco software. - * If the software was purchased under a paid Alfresco license, the terms of - * the paid license agreement will prevail. Otherwise, the software is - * provided under the following open source license terms: - * - * Alfresco is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Alfresco is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Alfresco. If not, see . - * #L% - */ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ package org.alfresco.repo.model.ml.tools; import java.util.ArrayList; @@ -45,7 +45,7 @@ import org.junit.experimental.categories.Category; public class ContentFilterLanguagesMapTest extends AbstractMultilingualTestCases { - public void testGetFilterLanguages() throws Exception + public void testGetFilterLanguages() { // get the list of content filter languages List lggs = contentFilterLanguagesService.getFilterLanguages(); @@ -57,7 +57,7 @@ public class ContentFilterLanguagesMapTest extends AbstractMultilingualTestCases try { lggs.add("NEW LOCALE"); - assertTrue("Add a value to the content filter language list is not permit, this list would be read only", false); + fail("Add a value to the content filter language list is not permit, this list would be read only"); } catch (Exception e) { @@ -67,7 +67,7 @@ public class ContentFilterLanguagesMapTest extends AbstractMultilingualTestCases try { lggs.remove(0); - assertTrue("Remove a value to the content filter language list is not permit, this list would be read only", false); + fail("Remove a value to the content filter language list is not permit, this list would be read only"); } catch (Exception e) { @@ -76,7 +76,7 @@ public class ContentFilterLanguagesMapTest extends AbstractMultilingualTestCases } @SuppressWarnings("unchecked") - public void testGetMissingLanguages() throws Exception + public void testGetMissingLanguages() { List lggs = contentFilterLanguagesService.getFilterLanguages(); @@ -97,7 +97,7 @@ public class ContentFilterLanguagesMapTest extends AbstractMultilingualTestCases assertEquals("Language list returned with the empty parameter corrupted", missingLggsEmpty.size(), lggs.size()); // get missing languages with a two locale list - List param = new ArrayList(); + List param = new ArrayList<>(); param.add(0, lggs.get(0)); param.add(1, lggs.get(1)); List missingLggsOk = contentFilterLanguagesService.getMissingLanguages(param); @@ -123,29 +123,29 @@ public class ContentFilterLanguagesMapTest extends AbstractMultilingualTestCases assertFalse("Language found : " + param.get(2), missingLggsWrong.contains(param.get(2))); } - public void testISOCodeConvertions() throws Exception + public void testISOCodeConversions() { // New ISO code list String[] newCode = {"he", "id", "yi"}; String[] oldCode = {"iw", "in", "ji"}; - Locale loc0 = new Locale(newCode[0]); - Locale loc1 = new Locale(newCode[1]); - Locale loc2 = new Locale(newCode[2]); + Locale loc0 = new Locale(oldCode[0]); + Locale loc1 = new Locale(oldCode[1]); + Locale loc2 = new Locale(oldCode[2]); - // Ensure that java.util.Locale has converted the new ISO code into new iso code - assertEquals("java.util.Locale Convertion not correct for " + newCode[0], oldCode[0], loc0.getLanguage()); - assertEquals("java.util.Locale Convertion not correct for " + newCode[1], oldCode[1], loc1.getLanguage()); - assertEquals("java.util.Locale Convertion not correct for " + newCode[2], oldCode[2], loc2.getLanguage()); + // Ensure that java.util.Locale has converted the old ISO code into new ISO code + // This conversion can be avoided by setting the java.locale.useOldISOCodes=true system property + assertEquals("java.util.Locale Conversion not correct for " + oldCode[0], newCode[0], loc0.getLanguage()); + assertEquals("java.util.Locale Conversion not correct for " + oldCode[1], newCode[1], loc1.getLanguage()); + assertEquals("java.util.Locale Conversion not correct for " + oldCode[2], newCode[2], loc2.getLanguage()); - // Ensure that the convertion is correcte - assertEquals("Convertion of new ISO codes not correct for " + newCode[0], oldCode[0], contentFilterLanguagesService.convertToOldISOCode(newCode[0])); - assertEquals("Convertion of new ISO codes not correct for " + newCode[1], oldCode[1], contentFilterLanguagesService.convertToOldISOCode(newCode[1])); - assertEquals("Convertion of new ISO codes not correct for " + newCode[2], oldCode[2], contentFilterLanguagesService.convertToOldISOCode(newCode[2])); + // Ensure that the conversion is correct + assertEquals("Conversion of new ISO codes not correct for " + newCode[0], oldCode[0], contentFilterLanguagesService.convertToOldISOCode(newCode[0])); + assertEquals("Conversion of new ISO codes not correct for " + newCode[1], oldCode[1], contentFilterLanguagesService.convertToOldISOCode(newCode[1])); + assertEquals("Conversion of new ISO codes not correct for " + newCode[2], oldCode[2], contentFilterLanguagesService.convertToOldISOCode(newCode[2])); - - assertEquals("Convertion of old ISO codes not correct for " + oldCode[0], newCode[0], contentFilterLanguagesService.convertToNewISOCode(oldCode[0])); - assertEquals("Convertion of old ISO codes not correct for " + oldCode[1], newCode[1], contentFilterLanguagesService.convertToNewISOCode(oldCode[1])); - assertEquals("Convertion of old ISO codes not correct for " + oldCode[2], newCode[2], contentFilterLanguagesService.convertToNewISOCode(oldCode[2])); + assertEquals("Conversion of old ISO codes not correct for " + oldCode[0], newCode[0], contentFilterLanguagesService.convertToNewISOCode(oldCode[0])); + assertEquals("Conversion of old ISO codes not correct for " + oldCode[1], newCode[1], contentFilterLanguagesService.convertToNewISOCode(oldCode[1])); + assertEquals("Conversion of old ISO codes not correct for " + oldCode[2], newCode[2], contentFilterLanguagesService.convertToNewISOCode(oldCode[2])); } } diff --git a/repository/src/test/java/org/alfresco/repo/module/DeprecatedModulesValidatorTest.java b/repository/src/test/java/org/alfresco/repo/module/DeprecatedModulesValidatorTest.java new file mode 100644 index 0000000000..83beaeb6dc --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/module/DeprecatedModulesValidatorTest.java @@ -0,0 +1,129 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.module; + +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.alfresco.service.cmr.module.ModuleDetails; +import org.alfresco.service.cmr.module.ModuleService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Test suite for the {@link DeprecatedModulesValidator} class. + * + * @author Domenico Sibilio + */ +@RunWith(MockitoJUnitRunner.class) +public class DeprecatedModulesValidatorTest +{ + + private static final String DEPRECATED_MODULE_1 = "deprecated-module-1"; + private static final String DEPRECATED_MODULE_2 = "deprecated-module-2"; + private static final String VALID_MODULE = "valid-module"; + private static final List DEPRECATED_MODULES = List.of(DEPRECATED_MODULE_1, DEPRECATED_MODULE_2); + @Mock + private ModuleService moduleService; + @Mock + private ModuleDetails moduleDetails; + private DeprecatedModulesValidator deprecatedModulesValidator; + + @Before + public void setUp() + { + deprecatedModulesValidator = new DeprecatedModulesValidator(moduleService, DEPRECATED_MODULES); + } + + @Test + public void shouldDoNothingWhenNoModulesAreFound() + { + when(moduleService.getAllModules()).thenReturn(null); + + deprecatedModulesValidator.onInit(); + + verify(moduleService).getAllModules(); + } + + @Test + public void shouldDoNothingWhenNoDeprecatedModulesAreFound() + { + when(moduleService.getAllModules()).thenReturn(List.of(moduleDetails)); + when(moduleDetails.getId()).thenReturn(VALID_MODULE); + + deprecatedModulesValidator.onInit(); + + verify(moduleService).getAllModules(); + verify(moduleDetails).getId(); + } + + @Test(expected = IllegalStateException.class) + public void shouldThrowExceptionWhenADeprecatedModuleIsFound() + { + when(moduleService.getAllModules()).thenReturn(List.of(moduleDetails)); + when(moduleDetails.getId()).thenReturn(DEPRECATED_MODULE_1); + + deprecatedModulesValidator.onInit(); + } + + @Test + public void shouldThrowExceptionWhenMultipleDeprecatedModulesAreFound() + { + when(moduleService.getAllModules()).thenReturn(List.of(moduleDetails, moduleDetails)); + when(moduleDetails.getId()).thenReturn(DEPRECATED_MODULE_1) + .thenReturn(DEPRECATED_MODULE_2); + + assertThrows("IllegalStateException should be thrown.", + IllegalStateException.class, + () -> deprecatedModulesValidator.onInit()); + + verify(moduleService).getAllModules(); + verify(moduleDetails, times(2)).getId(); + } + + @Test + public void shouldThrowExceptionWhenBothValidAndDeprecatedModulesAreFound() + { + when(moduleService.getAllModules()).thenReturn(List.of(moduleDetails, moduleDetails)); + when(moduleDetails.getId()).thenReturn(VALID_MODULE) + .thenReturn(DEPRECATED_MODULE_2); + + assertThrows("IllegalStateException should be thrown.", + IllegalStateException.class, + () -> deprecatedModulesValidator.onInit()); + + verify(moduleService).getAllModules(); + verify(moduleDetails, times(2)).getId(); + } + +} \ No newline at end of file diff --git a/repository/src/test/java/org/alfresco/repo/rendition2/AbstractRenditionIntegrationTest.java b/repository/src/test/java/org/alfresco/repo/rendition2/AbstractRenditionIntegrationTest.java index 8d17e8d8ae..2b8826659c 100644 --- a/repository/src/test/java/org/alfresco/repo/rendition2/AbstractRenditionIntegrationTest.java +++ b/repository/src/test/java/org/alfresco/repo/rendition2/AbstractRenditionIntegrationTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2021 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -57,7 +57,7 @@ import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.security.PersonService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; -import org.alfresco.transform.client.registry.TransformServiceRegistry; +import org.alfresco.transform.registry.TransformServiceRegistry; import org.alfresco.util.BaseSpringTest; import org.alfresco.util.GUID; import org.alfresco.util.PropertyMap; diff --git a/repository/src/test/java/org/alfresco/repo/rendition2/AbstractRenditionTest.java b/repository/src/test/java/org/alfresco/repo/rendition2/AbstractRenditionTest.java index b12695279b..003f13cd59 100644 --- a/repository/src/test/java/org/alfresco/repo/rendition2/AbstractRenditionTest.java +++ b/repository/src/test/java/org/alfresco/repo/rendition2/AbstractRenditionTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2018 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -29,8 +29,8 @@ import junit.framework.AssertionFailedError; import org.alfresco.repo.content.transform.TransformerDebug; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.thumbnail.ThumbnailDefinition; -import org.alfresco.transform.client.registry.AbstractTransformRegistry; -import org.alfresco.transform.client.registry.SupportedTransform; +import org.alfresco.transform.registry.AbstractTransformRegistry; +import org.alfresco.transform.registry.SupportedTransform; import org.alfresco.util.testing.category.DebugTests; import org.junit.Before; import org.junit.Test; diff --git a/repository/src/test/java/org/alfresco/repo/rendition2/LocalRenditionTest.java b/repository/src/test/java/org/alfresco/repo/rendition2/LocalRenditionTest.java index e951ee763a..32675a3732 100644 --- a/repository/src/test/java/org/alfresco/repo/rendition2/LocalRenditionTest.java +++ b/repository/src/test/java/org/alfresco/repo/rendition2/LocalRenditionTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2018 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -25,22 +25,15 @@ */ package org.alfresco.repo.rendition2; -import org.alfresco.transform.client.registry.AbstractTransformRegistry; -import org.alfresco.transform.client.registry.SupportedTransform; +import org.alfresco.transform.registry.AbstractTransformRegistry; import org.alfresco.util.testing.category.DebugTests; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.junit.experimental.categories.Category; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.alfresco.repo.content.MimetypeMap.MIMETYPE_TEXT_PLAIN; /** * Repeats quick file rendition tests with local transforms enabled but legacy transformers disabled. diff --git a/repository/src/test/java/org/alfresco/repo/rendition2/LocalSynchronousTransformClientIntegrationTest.java b/repository/src/test/java/org/alfresco/repo/rendition2/LocalSynchronousTransformClientIntegrationTest.java index 1997641982..b9c0a9007c 100644 --- a/repository/src/test/java/org/alfresco/repo/rendition2/LocalSynchronousTransformClientIntegrationTest.java +++ b/repository/src/test/java/org/alfresco/repo/rendition2/LocalSynchronousTransformClientIntegrationTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2019 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of diff --git a/repository/src/test/java/org/alfresco/repo/rendition2/LocalTransformServiceRegistryIntegrationTest.java b/repository/src/test/java/org/alfresco/repo/rendition2/LocalTransformServiceRegistryIntegrationTest.java index 03ef4c0aac..4689ad9abc 100644 --- a/repository/src/test/java/org/alfresco/repo/rendition2/LocalTransformServiceRegistryIntegrationTest.java +++ b/repository/src/test/java/org/alfresco/repo/rendition2/LocalTransformServiceRegistryIntegrationTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2019 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -26,8 +26,8 @@ package org.alfresco.repo.rendition2; import org.alfresco.repo.content.transform.LocalTransformServiceRegistry; -import org.alfresco.transform.client.registry.SupportedTransform; -import org.alfresco.transform.client.registry.TransformServiceRegistry; +import org.alfresco.transform.registry.SupportedTransform; +import org.alfresco.transform.registry.TransformServiceRegistry; import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; @@ -39,8 +39,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_IMAGE_JPEG; -import static org.alfresco.transform.client.model.Mimetype.MIMETYPE_IWORK_PAGES; +import static org.alfresco.transform.common.Mimetype.MIMETYPE_IMAGE_JPEG; +import static org.alfresco.transform.common.Mimetype.MIMETYPE_IWORK_PAGES; /** * Integration tests for {@link LocalTransformServiceRegistry} diff --git a/repository/src/test/java/org/alfresco/repo/rendition2/RenditionService2Test.java b/repository/src/test/java/org/alfresco/repo/rendition2/RenditionService2Test.java index 9c163cd253..22764a1d1d 100644 --- a/repository/src/test/java/org/alfresco/repo/rendition2/RenditionService2Test.java +++ b/repository/src/test/java/org/alfresco/repo/rendition2/RenditionService2Test.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2020 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -40,7 +40,7 @@ import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.rule.RuleService; import org.alfresco.service.namespace.QName; import org.alfresco.service.transaction.TransactionService; -import org.alfresco.transform.client.registry.TransformServiceRegistryImpl; +import org.alfresco.transform.registry.TransformServiceRegistryImpl; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/repository/src/test/java/org/alfresco/repo/rendition2/TestTransformServiceRegistry.java b/repository/src/test/java/org/alfresco/repo/rendition2/TestTransformServiceRegistry.java index 3e62a92378..37029ed6ed 100644 --- a/repository/src/test/java/org/alfresco/repo/rendition2/TestTransformServiceRegistry.java +++ b/repository/src/test/java/org/alfresco/repo/rendition2/TestTransformServiceRegistry.java @@ -25,8 +25,8 @@ */ package org.alfresco.repo.rendition2; -import org.alfresco.transform.client.model.config.CoreFunction; -import org.alfresco.transform.client.registry.TransformServiceRegistry; +import org.alfresco.transform.config.CoreFunction; +import org.alfresco.transform.registry.TransformServiceRegistry; import java.util.Map; diff --git a/repository/src/test/java/org/alfresco/repo/rule/RuleLinkTest.java b/repository/src/test/java/org/alfresco/repo/rule/RuleLinkTest.java index d0d1c26d84..380ff7cd9f 100644 --- a/repository/src/test/java/org/alfresco/repo/rule/RuleLinkTest.java +++ b/repository/src/test/java/org/alfresco/repo/rule/RuleLinkTest.java @@ -25,6 +25,12 @@ */ package org.alfresco.repo.rule; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; import org.alfresco.repo.action.evaluator.ComparePropertyValueEvaluator; @@ -51,12 +57,6 @@ import org.junit.Test; import org.junit.experimental.categories.Category; import org.springframework.transaction.annotation.Transactional; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - /** * Parameter definition implementation unit test. * @@ -364,6 +364,19 @@ public class RuleLinkTest extends BaseSpringTest assertEquals(expectedRuleSetNodeRef, ruleSetNodeRef); } + @Test + public void testGetRuleSetNodeForLinkedFolder() { + final Rule rule = createTestRule(false, "luke"); + this.ruleService.saveRule(folderOne, rule); + link(folderOne, folderTwo); + final NodeRef expectedRuleSetNodeRef = getRuleSetNode(folderOne); + + final NodeRef ruleSetNodeRef = ruleService.getRuleSetNode(folderTwo); + + assertNotNull(ruleSetNodeRef); + assertEquals(expectedRuleSetNodeRef, ruleSetNodeRef); + } + @Test public void testIsRuleSetAssociatedWithFolder() { @@ -425,6 +438,33 @@ public class RuleLinkTest extends BaseSpringTest assertFalse(associated); } + @Test + public void testIsRuleSetShared() + { + final Rule rule = createTestRule(false, "luke"); + this.ruleService.saveRule(folderOne, rule); + link(folderOne, folderTwo); + final NodeRef ruleSetNodeRef = ruleService.getRuleSetNode(folderOne); + + // when + final boolean shared = ruleService.isRuleSetShared(ruleSetNodeRef); + + assertTrue(shared); + } + + @Test + public void testIsRuleSetNotShared() + { + final Rule rule = createTestRule(false, "luke"); + this.ruleService.saveRule(folderOne, rule); + final NodeRef ruleSetNodeRef = ruleService.getRuleSetNode(folderOne); + + // when + final boolean shared = ruleService.isRuleSetShared(ruleSetNodeRef); + + assertFalse(shared); + } + protected Rule createTestRule(boolean isAppliedToChildren, String title) { // Rule properties diff --git a/repository/src/test/java/org/alfresco/repo/rule/RuleServiceCoverageTest.java b/repository/src/test/java/org/alfresco/repo/rule/RuleServiceCoverageTest.java index c4bcad5874..27ee3b35d6 100644 --- a/repository/src/test/java/org/alfresco/repo/rule/RuleServiceCoverageTest.java +++ b/repository/src/test/java/org/alfresco/repo/rule/RuleServiceCoverageTest.java @@ -99,6 +99,7 @@ import org.alfresco.service.namespace.RegexQNamePattern; import org.alfresco.service.transaction.TransactionService; import org.alfresco.test_category.OwnJVMTestsCategory; import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.GUID; import org.junit.experimental.categories.Category; import org.springframework.context.ApplicationContext; import org.springframework.util.StopWatch; @@ -194,25 +195,26 @@ public class RuleServiceCoverageTest extends TestCase ContentModel.ASSOC_CHILDREN, ContentModel.TYPE_CONTAINER).getChildRef(); } - - private Rule createRule( - String ruleTypeName, - String actionName, - Map actionParams, - String conditionName, - Map conditionParams) - { - Rule rule = new Rule(); - rule.setRuleType(ruleTypeName); - - Action action = this.actionService.createAction(actionName, actionParams); + + private Rule createRule( + String ruleTypeName, + String actionName, + Map actionParams, + String conditionName, + Map conditionParams) + { + Rule rule = new Rule(); + rule.setTitle(GUID.generate()); + rule.setRuleType(ruleTypeName); + + Action action = this.actionService.createAction(actionName, actionParams); ActionCondition condition = this.actionService.createActionCondition(conditionName, conditionParams); action.addActionCondition(condition); - rule.setAction(action); - + rule.setAction(action); + return rule; - } - + } + /** * Create the categories used in the tests */ diff --git a/repository/src/test/java/org/alfresco/repo/rule/RuleServiceImplUnitTest.java b/repository/src/test/java/org/alfresco/repo/rule/RuleServiceImplUnitTest.java new file mode 100644 index 0000000000..8d03048175 --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/rule/RuleServiceImplUnitTest.java @@ -0,0 +1,759 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.rule; + +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; + +import static org.alfresco.model.ContentModel.ASSOC_CONTAINS; +import static org.alfresco.model.ContentModel.ASSOC_MEMBER; +import static org.alfresco.model.ContentModel.TYPE_CONTENT; +import static org.alfresco.model.ContentModel.TYPE_FOLDER; +import static org.alfresco.repo.rule.RuleModel.ASPECT_IGNORE_INHERITED_RULES; +import static org.alfresco.repo.rule.RuleModel.ASSOC_ACTION; +import static org.alfresco.repo.rule.RuleModel.ASSOC_RULE_FOLDER; +import static org.alfresco.repo.rule.RuleModel.TYPE_RULE; +import static org.alfresco.service.cmr.security.AccessStatus.ALLOWED; +import static org.alfresco.service.cmr.security.AccessStatus.DENIED; +import static org.alfresco.service.namespace.RegexQNamePattern.MATCH_ALL; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.alfresco.repo.action.RuntimeActionService; +import org.alfresco.repo.cache.SimpleCache; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionServiceException; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.rule.Rule; +import org.alfresco.service.cmr.rule.RuleService; +import org.alfresco.service.cmr.rule.RuleServiceException; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.collections.MapUtils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +/** Unit tests for {@link RuleServiceImpl}. */ +public class RuleServiceImplUnitTest +{ + private static final NodeRef FOLDER_NODE = new NodeRef("folder://node/"); + private static final NodeRef RULE_SET_NODE = new NodeRef("rule://set/node"); + private static final NodeRef RULE_NODE = new NodeRef("rule://node/"); + private static final NodeRef ACTION_NODE = new NodeRef("action://node/"); + @InjectMocks + private RuleService ruleService = new RuleServiceImpl(); + @Mock + private NodeService nodeService; + @Mock + private PermissionService permissionService; + @Mock + private SimpleCache nodeRulesCache; + @Mock + private NodeService runtimeNodeService; + @Mock + private RuntimeActionService runtimeActionService; + @Mock + private DictionaryService dictionaryService; + @Mock + private Rule mockRule; + @Mock + private Action mockAction; + + @Before + public void setUp() + { + openMocks(this); + + when(dictionaryService.isSubClass(TYPE_FOLDER, TYPE_FOLDER)).thenReturn(true); + when(dictionaryService.isSubClass(TYPE_CONTENT, TYPE_FOLDER)).thenReturn(false); + when(permissionService.hasReadPermission(any())).thenReturn(ALLOWED); + } + + @Test + public void saveRule() + { + when(permissionService.hasPermission(FOLDER_NODE, PermissionService.CHANGE_PERMISSIONS)).thenReturn(ALLOWED); + when(nodeService.exists(FOLDER_NODE)).thenReturn(true); + ChildAssociationRef ruleSet = mock(ChildAssociationRef.class); + when(ruleSet.getChildRef()).thenReturn(RULE_SET_NODE); + when(runtimeNodeService.getChildAssocs(FOLDER_NODE, ASSOC_RULE_FOLDER, ASSOC_RULE_FOLDER)).thenReturn(List.of(ruleSet)); + ChildAssociationRef ruleAssociation = mock(ChildAssociationRef.class); + when(ruleAssociation.getChildRef()).thenReturn(RULE_NODE); + when(nodeService.createNode(eq(RULE_SET_NODE), eq(ASSOC_CONTAINS), any(QName.class), eq(TYPE_RULE))).thenReturn(ruleAssociation); + // Set the rule title and action. + when(mockRule.getTitle()).thenReturn("Rule title"); + when(mockRule.getAction()).thenReturn(mockAction); + when(runtimeActionService.createActionNodeRef(mockAction, RULE_NODE, ASSOC_ACTION, ASSOC_ACTION)).thenReturn(ACTION_NODE); + + // Call the method under test. + ruleService.saveRule(FOLDER_NODE, mockRule); + + then(nodeService).should(times(2)).hasAspect(FOLDER_NODE, RuleModel.ASPECT_RULES); + then(nodeService).should().exists(FOLDER_NODE); + then(nodeService).should().addAspect(FOLDER_NODE, RuleModel.ASPECT_RULES, null); + then(nodeService).should().createNode(eq(RULE_SET_NODE), eq(ASSOC_CONTAINS), any(QName.class), eq(TYPE_RULE)); + then(nodeService).should(atLeastOnce()).setProperty(eq(RULE_NODE), any(QName.class), nullable(Serializable.class)); + then(nodeService).should().getChildAssocs(RULE_NODE, RuleModel.ASSOC_ACTION, RuleModel.ASSOC_ACTION); + verifyNoMoreInteractions(nodeService); + } + + @Test + public void saveRule_missingAction() + { + when(permissionService.hasPermission(FOLDER_NODE, PermissionService.CHANGE_PERMISSIONS)).thenReturn(ALLOWED); + when(nodeService.exists(FOLDER_NODE)).thenReturn(true); + ChildAssociationRef ruleSet = mock(ChildAssociationRef.class); + when(ruleSet.getChildRef()).thenReturn(RULE_SET_NODE); + when(runtimeNodeService.getChildAssocs(FOLDER_NODE, ASSOC_RULE_FOLDER, ASSOC_RULE_FOLDER)).thenReturn(List.of(ruleSet)); + ChildAssociationRef ruleAssociation = mock(ChildAssociationRef.class); + when(nodeService.createNode(eq(RULE_SET_NODE), eq(ASSOC_CONTAINS), any(QName.class), eq(TYPE_RULE))).thenReturn(ruleAssociation); + // Set the title and no action for the rule. + when(mockRule.getTitle()).thenReturn("Rule title"); + when(mockRule.getAction()).thenReturn(null); + + // Call the method under test. + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> ruleService.saveRule(FOLDER_NODE, mockRule)); + } + + @Test + public void saveRule_missingTitle() + { + when(permissionService.hasPermission(FOLDER_NODE, PermissionService.CHANGE_PERMISSIONS)).thenReturn(ALLOWED); + when(nodeService.exists(FOLDER_NODE)).thenReturn(true); + ChildAssociationRef ruleSet = mock(ChildAssociationRef.class); + when(ruleSet.getChildRef()).thenReturn(RULE_SET_NODE); + when(runtimeNodeService.getChildAssocs(FOLDER_NODE, ASSOC_RULE_FOLDER, ASSOC_RULE_FOLDER)).thenReturn(List.of(ruleSet)); + ChildAssociationRef ruleAssociation = mock(ChildAssociationRef.class); + when(nodeService.createNode(eq(RULE_SET_NODE), eq(ASSOC_CONTAINS), any(QName.class), eq(TYPE_RULE))).thenReturn(ruleAssociation); + // The rule has an empty title. + when(mockRule.getTitle()).thenReturn(""); + + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> ruleService.saveRule(FOLDER_NODE, mockRule)); + } + + @Test + public void saveRule_errorIfFolderHasMultipleRuleSets() + { + when(permissionService.hasPermission(FOLDER_NODE, PermissionService.CHANGE_PERMISSIONS)).thenReturn(ALLOWED); + when(nodeService.exists(FOLDER_NODE)).thenReturn(true); + // Simulate a folder node with several rule sets. + ChildAssociationRef childA = mock(ChildAssociationRef.class); + ChildAssociationRef childB = mock(ChildAssociationRef.class); + when(runtimeNodeService.getChildAssocs( + FOLDER_NODE, + ASSOC_RULE_FOLDER, + ASSOC_RULE_FOLDER)).thenReturn(List.of(childA, childB)); + + assertThatExceptionOfType(ActionServiceException.class).isThrownBy(() -> ruleService.saveRule(FOLDER_NODE, mockRule)); + } + + @Test + public void saveRule_nodeDoesNotExist() + { + when(permissionService.hasPermission(FOLDER_NODE, PermissionService.CHANGE_PERMISSIONS)).thenReturn(ALLOWED); + when(nodeService.exists(FOLDER_NODE)).thenReturn(false); + + assertThatExceptionOfType(RuleServiceException.class).isThrownBy(() -> ruleService.saveRule(FOLDER_NODE, mockRule)); + } + + @Test + public void saveRule_accessDenied() + { + when(permissionService.hasPermission(FOLDER_NODE, PermissionService.CHANGE_PERMISSIONS)).thenReturn(DENIED); + + assertThatExceptionOfType(RuleServiceException.class).isThrownBy(() -> ruleService.saveRule(FOLDER_NODE, mockRule)); + } + + @Test + public void testGetRuleSetNode() + { + given(runtimeNodeService.getChildAssocs(any(), any(), any())).willReturn(List.of(createAssociation(FOLDER_NODE, RULE_SET_NODE))); + + // when + final NodeRef actualNode = ruleService.getRuleSetNode(FOLDER_NODE); + + then(runtimeNodeService).should().getChildAssocs(FOLDER_NODE, ASSOC_RULE_FOLDER, ASSOC_RULE_FOLDER); + then(runtimeNodeService).shouldHaveNoMoreInteractions(); + then(nodeService).shouldHaveNoInteractions(); + assertThat(actualNode).isNotNull(); + } + + @Test + public void testGetRuleSetNode_emptyAssociation() + { + given(runtimeNodeService.getChildAssocs(any(), any(), any())).willReturn(emptyList()); + + // when + final NodeRef actualNode = ruleService.getRuleSetNode(FOLDER_NODE); + + then(runtimeNodeService).should().getChildAssocs(FOLDER_NODE, ASSOC_RULE_FOLDER, ASSOC_RULE_FOLDER); + then(runtimeNodeService).shouldHaveNoMoreInteractions(); + then(nodeService).shouldHaveNoInteractions(); + assertThat(actualNode).isNull(); + } + + @Test + public void testGetRuleSetNode_notPrimaryAssociation() + { + given(runtimeNodeService.getChildAssocs(any(), any(), any())).willReturn(List.of(createAssociation(FOLDER_NODE, RULE_SET_NODE, false))); + + // when + final NodeRef actualNode = ruleService.getRuleSetNode(FOLDER_NODE); + + then(runtimeNodeService).should().getChildAssocs(FOLDER_NODE, ASSOC_RULE_FOLDER, ASSOC_RULE_FOLDER); + then(runtimeNodeService).shouldHaveNoMoreInteractions(); + then(nodeService).shouldHaveNoInteractions(); + assertThat(actualNode).isNotNull(); + } + + @Test + public void testIsRuleSetAssociatedWithFolder() + { + given(runtimeNodeService.getParentAssocs(any(), any(), any())).willReturn(List.of(createAssociation(FOLDER_NODE, RULE_SET_NODE))); + + // when + boolean associated = ruleService.isRuleSetAssociatedWithFolder(RULE_SET_NODE, FOLDER_NODE); + + then(runtimeNodeService).should().getParentAssocs(RULE_SET_NODE, ASSOC_RULE_FOLDER, ASSOC_RULE_FOLDER); + then(runtimeNodeService).should().hasAspect(FOLDER_NODE, ASPECT_IGNORE_INHERITED_RULES); + then(runtimeNodeService).should().getParentAssocs(FOLDER_NODE); + then(runtimeNodeService).shouldHaveNoMoreInteractions(); + then(nodeService).shouldHaveNoInteractions(); + assertThat(associated).isTrue(); + } + + @Test + public void testIsRuleSetAssociatedWithFolder_emptyAssociation() + { + given(runtimeNodeService.getParentAssocs(any(), any(), any())).willReturn(emptyList()); + + // when + boolean associated = ruleService.isRuleSetAssociatedWithFolder(RULE_SET_NODE, FOLDER_NODE); + + then(runtimeNodeService).should().getParentAssocs(RULE_SET_NODE, ASSOC_RULE_FOLDER, ASSOC_RULE_FOLDER); + then(runtimeNodeService).should().hasAspect(FOLDER_NODE, ASPECT_IGNORE_INHERITED_RULES); + then(runtimeNodeService).should().getParentAssocs(FOLDER_NODE); + then(runtimeNodeService).shouldHaveNoMoreInteractions(); + then(nodeService).shouldHaveNoInteractions(); + assertThat(associated).isFalse(); + } + + @Test + public void testIsRuleSetAssociatedWithFolder_improperAssociation() + { + final NodeRef fakeFolderNode = new NodeRef("folder://node/fake"); + given(runtimeNodeService.getParentAssocs(any(), any(), any())).willReturn(List.of(createAssociation(fakeFolderNode, RULE_SET_NODE))); + + // when + boolean associated = ruleService.isRuleSetAssociatedWithFolder(RULE_SET_NODE, FOLDER_NODE); + + then(runtimeNodeService).should().getParentAssocs(RULE_SET_NODE, ASSOC_RULE_FOLDER, ASSOC_RULE_FOLDER); + then(runtimeNodeService).should().hasAspect(FOLDER_NODE, ASPECT_IGNORE_INHERITED_RULES); + then(runtimeNodeService).should().getParentAssocs(FOLDER_NODE); + then(runtimeNodeService).shouldHaveNoMoreInteractions(); + then(nodeService).shouldHaveNoInteractions(); + assertThat(associated).isFalse(); + } + + /** + * Check that a rule set is associated with the folder in the following case: + *
+     *     parent --[link]-> rule set <-[owned]-- owningFolder
+     *     +- child
+     * 
+ */ + @Test + public void testIsRuleSetAssociatedWithFolder_inheritedLinkedAssociation() + { + // The rule is owned by one node. + NodeRef owningFolder = new NodeRef("owning://node/"); + // The rule is linked to by the parent node. + NodeRef parent = new NodeRef("parent://node/"); + List ruleAssociations = List.of(createAssociation(owningFolder, RULE_SET_NODE), createAssociation(parent, RULE_SET_NODE)); + given(runtimeNodeService.getParentAssocs(RULE_SET_NODE, ASSOC_RULE_FOLDER, ASSOC_RULE_FOLDER)).willReturn(ruleAssociations); + // The parent and the child both supply rule sets. + given(runtimeNodeService.getParentAssocs(FOLDER_NODE)).willReturn(List.of(createAssociation(parent, FOLDER_NODE))); + + // when + boolean associated = ruleService.isRuleSetAssociatedWithFolder(RULE_SET_NODE, FOLDER_NODE); + + then(runtimeNodeService).should().getParentAssocs(RULE_SET_NODE, ASSOC_RULE_FOLDER, ASSOC_RULE_FOLDER); + then(runtimeNodeService).should().hasAspect(FOLDER_NODE, ASPECT_IGNORE_INHERITED_RULES); + then(runtimeNodeService).should().getParentAssocs(FOLDER_NODE); + then(runtimeNodeService).should().hasAspect(parent, ASPECT_IGNORE_INHERITED_RULES); + then(runtimeNodeService).should().getParentAssocs(parent); + then(runtimeNodeService).shouldHaveNoMoreInteractions(); + then(nodeService).shouldHaveNoInteractions(); + assertThat(associated).isTrue(); + } + + @Test + public void testIsRuleAssociatedWithRuleSet() + { + given(runtimeNodeService.getParentAssocs(any())).willReturn(List.of(createAssociation(RULE_SET_NODE, RULE_NODE))); + + // when + boolean associated = ruleService.isRuleAssociatedWithRuleSet(RULE_NODE, RULE_SET_NODE); + + then(runtimeNodeService).should().getParentAssocs(RULE_NODE); + then(runtimeNodeService).shouldHaveNoMoreInteractions(); + then(nodeService).shouldHaveNoInteractions(); + assertThat(associated).isTrue(); + } + + @Test + public void testIsRuleAssociatedWithRuleSet_emptyAssociation() + { + given(runtimeNodeService.getParentAssocs(any())).willReturn(emptyList()); + + // when + boolean associated = ruleService.isRuleAssociatedWithRuleSet(RULE_NODE, RULE_SET_NODE); + + then(runtimeNodeService).should().getParentAssocs(RULE_NODE); + then(runtimeNodeService).shouldHaveNoMoreInteractions(); + then(nodeService).shouldHaveNoInteractions(); + assertThat(associated).isFalse(); + } + + @Test + public void testIsRuleAssociatedWithRuleSet_improperAssociation() + { + final NodeRef fakeRuleSetNode = new NodeRef("rule://set/node/fake"); + given(runtimeNodeService.getParentAssocs(any())).willReturn(List.of(createAssociation(fakeRuleSetNode, RULE_NODE))); + + // when + boolean associated = ruleService.isRuleAssociatedWithRuleSet(RULE_NODE, RULE_SET_NODE); + + then(runtimeNodeService).should().getParentAssocs(RULE_NODE); + then(runtimeNodeService).shouldHaveNoMoreInteractions(); + then(nodeService).shouldHaveNoInteractions(); + assertThat(associated).isFalse(); + } + + @Test + public void testIsRuleSetShared() + { + given(runtimeNodeService.getParentAssocs(any())).willReturn(List.of(createAssociation(FOLDER_NODE, RULE_SET_NODE, false))); + + // when + boolean shared = ruleService.isRuleSetShared(RULE_SET_NODE); + + then(runtimeNodeService).should().getParentAssocs(RULE_SET_NODE); + then(runtimeNodeService).shouldHaveNoMoreInteractions(); + then(nodeService).shouldHaveNoInteractions(); + assertThat(shared).isTrue(); + } + + @Test + public void testIsRuleSetShared_notShared() + { + given(runtimeNodeService.getParentAssocs(any())).willReturn(List.of(createAssociation(FOLDER_NODE, RULE_SET_NODE))); + + // when + boolean shared = ruleService.isRuleSetShared(RULE_SET_NODE); + + then(runtimeNodeService).should().getParentAssocs(RULE_SET_NODE); + then(runtimeNodeService).shouldHaveNoMoreInteractions(); + then(nodeService).shouldHaveNoInteractions(); + assertThat(shared).isFalse(); + } + + private static ChildAssociationRef createAssociation(final NodeRef parentRef, final NodeRef childRef) + { + return createAssociation(parentRef, childRef, true); + } + + private static ChildAssociationRef createAssociation(final NodeRef parentRef, final NodeRef childRef, final boolean isPrimary) + { + return new ChildAssociationRef(null, parentRef, null, childRef, isPrimary, 1); + } + + /** Check that a straight chain of nodes is traversed correctly. */ + @Test + public void testGetNodesSupplyingRuleSets_chain() + { + Map nodes = createParentChildHierarchy("A,B", "B,C", "C,D", "D,E"); + + List actual = ruleService.getNodesSupplyingRuleSets(nodes.get("E")); + + Map invertedMap = MapUtils.invertMap(nodes); + String nodeNames = actual.stream().map(invertedMap::get).collect(joining(",")); + + assertEquals("A,B,C,D,E", nodeNames); + } + + /** Check that ordered parents are returned in the correct order. */ + @Test + public void testGetNodesSupplyingRuleSets_multipleParents() + { + Map nodes = createParentChildHierarchy("A,E", "B,E", "C,E", "D,E"); + + List actual = ruleService.getNodesSupplyingRuleSets(nodes.get("E")); + + Map invertedMap = MapUtils.invertMap(nodes); + String nodeNames = actual.stream().map(invertedMap::get).collect(joining(",")); + + assertEquals("A,B,C,D,E", nodeNames); + } + + /** Check that the ASPECT_IGNORE_INHERITED_RULES aspect breaks the chain. */ + @Test + public void testGetNodesSupplyingRuleSets_brokenChain() + { + Map nodes = createParentChildHierarchy("A,B", "B,C", "C,D", "D,E"); + given(runtimeNodeService.hasAspect(nodes.get("C"), ASPECT_IGNORE_INHERITED_RULES)).willReturn(true); + + List actual = ruleService.getNodesSupplyingRuleSets(nodes.get("E")); + + Map invertedMap = MapUtils.invertMap(nodes); + String nodeNames = actual.stream().map(invertedMap::get).collect(joining(",")); + + assertEquals("C,D,E", nodeNames); + } + + /** Check that the user group hierarchy is not traversed. */ + @Test + public void testGetNodesSupplyingRuleSets_userGroupHierarchy() + { + Map nodes = createParentChildHierarchy("A,B", "B,C", "C,D", "D,E"); + // Replace the B,C association with a user group membership association. + ChildAssociationRef memberAssoc = new ChildAssociationRef(ASSOC_MEMBER, nodes.get("B"), TYPE_FOLDER, nodes.get("C")); + given(runtimeNodeService.getParentAssocs(nodes.get("C"))).willReturn(List.of(memberAssoc)); + + List actual = ruleService.getNodesSupplyingRuleSets(nodes.get("E")); + + Map invertedMap = MapUtils.invertMap(nodes); + String nodeNames = actual.stream().map(invertedMap::get).collect(joining(",")); + + assertEquals("C,D,E", nodeNames); + } + + /** Check that a cycle doesn't cause a problem. */ + @Test + public void testGetNodesSupplyingRuleSets_infiniteCycle() + { + Map nodes = createParentChildHierarchy("A,B", "B,C", "C,A"); + + List actual = ruleService.getNodesSupplyingRuleSets(nodes.get("C")); + + Map invertedMap = MapUtils.invertMap(nodes); + String nodeNames = actual.stream().map(invertedMap::get).collect(joining(",")); + + assertEquals("A,B,C", nodeNames); + } + + /** Check that a diamond of nodes is traversed correctly. */ + @Test + public void testGetNodesSupplyingRuleSets_diamond() + { + Map nodes = createParentChildHierarchy("A,B", "A,C", "B,D", "C,D"); + + List actual = ruleService.getNodesSupplyingRuleSets(nodes.get("D")); + + Map invertedMap = MapUtils.invertMap(nodes); + String nodeNames = actual.stream().map(invertedMap::get).collect(joining(",")); + + assertEquals("A,B,C,D", nodeNames); + } + + /** + * Check that hierarchy of nodes is traversed correctly. Parent-child associations are created in alphabetical order. + *
+     *     A
+     *    /|\
+     *   B C D
+     *   | |\|
+     *   E | F
+     *    \|/
+     *     G
+     * 
+ */ + @Test + public void testGetNodesSupplyingRuleSets_alphabetical() + { + Map nodes = createParentChildHierarchy("A,B", "A,C", "A,D", "B,E", "C,F", "C,G", "D,F", "E,G", "F,G"); + + List actual = ruleService.getNodesSupplyingRuleSets(nodes.get("G")); + + Map invertedMap = MapUtils.invertMap(nodes); + String nodeNames = actual.stream().map(invertedMap::get).collect(joining(",")); + + assertEquals("A,C,B,E,D,F,G", nodeNames); + } + + /** + * Check that hierarchy of nodes is traversed correctly. Parent-child associations are created in reverse alphabetical order. + *
+     *     A
+     *    /|\
+     *   B C D
+     *   | |\|
+     *   E | F
+     *    \|/
+     *     G
+     * 
+ */ + @Test + public void testGetNodesSupplyingRuleSets_reversedAssociationOrder() + { + Map nodes = createParentChildHierarchy("F,G", "E,G", "D,F", "C,G", "C,F", "B,E", "A,D", "A,C", "A,B"); + + List actual = ruleService.getNodesSupplyingRuleSets(nodes.get("G")); + + Map invertedMap = MapUtils.invertMap(nodes); + String nodeNames = actual.stream().map(invertedMap::get).collect(joining(",")); + + assertEquals("A,D,C,F,B,E,G", nodeNames); + } + + /** + * Create a mock hierarchy of nodes using the supplied parent child associations. + * + * @param parentChildAssociations A list of strings of the form "Parent,Child". Associations will be created in this order. + * @return A map from the node name to the new NodeRef object. + */ + private Map createParentChildHierarchy(String... parentChildAssociations) + { + // Find all the node names mentioned. + Set nodeNames = new HashSet<>(); + List.of(parentChildAssociations).forEach(parentChildAssociation -> { + String[] parentChildPair = parentChildAssociation.split(","); + nodeNames.addAll(List.of(parentChildPair)); + }); + // Create the NodeRefs. + Map nodeRefMap = nodeNames.stream().collect( + Collectors.toMap(nodeName -> nodeName, nodeName -> new NodeRef("node://" + nodeName + "/"))); + // Mock the associations. + nodeNames.forEach(nodeName -> { + NodeRef nodeRef = nodeRefMap.get(nodeName); + List parentAssocs = List.of(parentChildAssociations) + .stream() + .filter(assoc -> assoc.endsWith(nodeName)) + .map(assoc -> assoc.split(",")[0]) + .map(nodeRefMap::get) + .map(parentRef -> new ChildAssociationRef(ASSOC_CONTAINS, parentRef, TYPE_FOLDER, nodeRef)) + .collect(toList()); + given(runtimeNodeService.getParentAssocs(nodeRef)).willReturn(parentAssocs); + }); + return nodeRefMap; + } + + /** Check that getFoldersInheritingRuleSet returns a child folder. */ + @Test + public void testGetFoldersInheritingRuleSet() + { + NodeRef parent = new NodeRef("parent://node/"); + NodeRef ruleSetNode = new NodeRef("rule://set/"); + ChildAssociationRef ruleSetAssociation = mock(ChildAssociationRef.class); + given(runtimeNodeService.getParentAssocs(ruleSetNode)).willReturn(List.of(ruleSetAssociation)); + given(ruleSetAssociation.getParentRef()).willReturn(parent); + NodeRef child = new NodeRef("child://node/"); + ChildAssociationRef childAssocMock = mock(ChildAssociationRef.class); + given(runtimeNodeService.getChildAssocs(parent, ASSOC_CONTAINS, MATCH_ALL)).willReturn(List.of(childAssocMock)); + given(childAssocMock.getChildRef()).willReturn(child); + given(runtimeNodeService.getType(child)).willReturn(TYPE_FOLDER); + + List actual = ruleService.getFoldersInheritingRuleSet(ruleSetNode, 100); + + assertEquals("Unexpected list of inheriting folders.", List.of(child), actual); + } + + /** Check that getFoldersInheritingRuleSet omits a child folder if IGNORE_INHERITED_RULES is applied. */ + @Test + public void testGetFoldersInheritingRuleSet_ignoreInheritedRules() + { + NodeRef parent = new NodeRef("parent://node/"); + NodeRef ruleSetNode = new NodeRef("rule://set/"); + ChildAssociationRef ruleSetAssociation = mock(ChildAssociationRef.class); + given(runtimeNodeService.getParentAssocs(ruleSetNode)).willReturn(List.of(ruleSetAssociation)); + given(ruleSetAssociation.getParentRef()).willReturn(parent); + NodeRef child = new NodeRef("child://node/"); + ChildAssociationRef childAssocMock = mock(ChildAssociationRef.class); + given(runtimeNodeService.getChildAssocs(parent, ASSOC_CONTAINS, MATCH_ALL)).willReturn(List.of(childAssocMock)); + given(childAssocMock.getChildRef()).willReturn(child); + given(runtimeNodeService.getType(child)).willReturn(TYPE_FOLDER); + given(runtimeNodeService.hasAspect(child, ASPECT_IGNORE_INHERITED_RULES)).willReturn(true); + + List actual = ruleService.getFoldersInheritingRuleSet(ruleSetNode, 100); + + assertEquals("Unexpected list of inheriting folders.", emptyList(), actual); + } + + /** Check that getFoldersInheritingRuleSet only returns at most the requested number of folders. */ + @Test + public void testGetFoldersExceedsLimit() + { + NodeRef root = new NodeRef("root://node/"); + NodeRef ruleSetNode = new NodeRef("rule://set/"); + ChildAssociationRef ruleSetAssociation = mock(ChildAssociationRef.class); + given(runtimeNodeService.getParentAssocs(ruleSetNode)).willReturn(List.of(ruleSetAssociation)); + given(ruleSetAssociation.getParentRef()).willReturn(root); + // Create a chain of ancestors starting from the root that is 10 folders deep. + List nodeChain = new ArrayList<>(); + nodeChain.add(root); + IntStream.range(0, 10).forEach(index -> { + NodeRef parent = nodeChain.get(nodeChain.size() - 1); + NodeRef child = new NodeRef("chain://node/" + index); + nodeChain.add(child); + ChildAssociationRef childAssocMock = mock(ChildAssociationRef.class); + given(runtimeNodeService.getChildAssocs(parent, ASSOC_CONTAINS, MATCH_ALL)).willReturn(List.of(childAssocMock)); + given(childAssocMock.getChildRef()).willReturn(child); + given(runtimeNodeService.getType(child)).willReturn(TYPE_FOLDER); + }); + + // Request at most 9 folders inheriting the rule. + List actual = ruleService.getFoldersInheritingRuleSet(ruleSetNode, 9); + + // Check we don't get the root node or the final descendant folder. + assertEquals("Unexpected list of inheriting folders.", nodeChain.subList(1, 10), actual); + } + + /** Check that getFoldersInheritingRuleSet doesn't include documents. */ + @Test + public void testGetFoldersInheritingRuleSet_ignoreFiles() + { + NodeRef parent = new NodeRef("parent://node/"); + NodeRef ruleSetNode = new NodeRef("rule://set/"); + ChildAssociationRef ruleSetAssociation = mock(ChildAssociationRef.class); + given(runtimeNodeService.getParentAssocs(ruleSetNode)).willReturn(List.of(ruleSetAssociation)); + given(ruleSetAssociation.getParentRef()).willReturn(parent); + NodeRef childDocument = new NodeRef("child://document/"); + ChildAssociationRef childAssocMock = mock(ChildAssociationRef.class); + given(runtimeNodeService.getChildAssocs(parent, ASSOC_CONTAINS, MATCH_ALL)).willReturn(List.of(childAssocMock)); + given(childAssocMock.getChildRef()).willReturn(childDocument); + given(runtimeNodeService.getType(childDocument)).willReturn(TYPE_CONTENT); + + List actual = ruleService.getFoldersInheritingRuleSet(ruleSetNode, 100); + + assertEquals("Unexpected list of inheriting folders.", emptyList(), actual); + } + + /** + * Check that getFoldersInheritingRuleSet does not include folders that the user doesn't have access to. + *

+ * This test uses a chain of three folders: + *

+     *     grandparent - owns the rule set
+     *     parent - user does not have read access
+     *     child - user _does_ have read access
+     * 
+ */ + @Test + public void testGetFoldersInheritingRuleSet_omitFoldersWithoutReadPermission() + { + NodeRef grandparent = new NodeRef("grandparent://node/"); + NodeRef ruleSetNode = new NodeRef("rule://set/"); + ChildAssociationRef ruleSetAssociation = mock(ChildAssociationRef.class); + given(runtimeNodeService.getParentAssocs(ruleSetNode)).willReturn(List.of(ruleSetAssociation)); + given(ruleSetAssociation.getParentRef()).willReturn(grandparent); + NodeRef parent = new NodeRef("parent://node/"); + ChildAssociationRef parentAssocMock = mock(ChildAssociationRef.class); + given(runtimeNodeService.getChildAssocs(grandparent, ASSOC_CONTAINS, MATCH_ALL)).willReturn(List.of(parentAssocMock)); + given(parentAssocMock.getChildRef()).willReturn(parent); + given(runtimeNodeService.getType(parent)).willReturn(TYPE_FOLDER); + NodeRef child = new NodeRef("child://node/"); + ChildAssociationRef childAssocMock = mock(ChildAssociationRef.class); + given(runtimeNodeService.getChildAssocs(grandparent, ASSOC_CONTAINS, MATCH_ALL)).willReturn(List.of(childAssocMock)); + given(childAssocMock.getChildRef()).willReturn(child); + given(runtimeNodeService.getType(child)).willReturn(TYPE_FOLDER); + + // The current user doesn't have permission to view the parent node. + given(permissionService.hasReadPermission(parent)).willReturn(DENIED); + + List actual = ruleService.getFoldersInheritingRuleSet(ruleSetNode, 100); + + assertEquals("Unexpected list of inheriting folders.", List.of(child), actual); + } + + /** Check that a linked folder can be retrieved from a rule set node. */ + @Test + public void testGetFoldersLinkingToRuleSet() + { + NodeRef ruleSetNode = new NodeRef("rule://set/"); + NodeRef owningFolder = new NodeRef("owning://folder/"); + ChildAssociationRef owningAssocMock = mock(ChildAssociationRef.class); + given(owningAssocMock.getParentRef()).willReturn(owningFolder); + given(nodeService.getPrimaryParent(ruleSetNode)).willReturn(owningAssocMock); + // Simulate a folder linking to the rule set. + NodeRef linkingFolder = new NodeRef("linking://folder/"); + ChildAssociationRef linkingAssocMock = mock(ChildAssociationRef.class); + given(linkingAssocMock.getParentRef()).willReturn(linkingFolder); + given(nodeService.getParentAssocs(ruleSetNode)).willReturn(List.of(owningAssocMock, linkingAssocMock)); + + List linkingFolders = ruleService.getFoldersLinkingToRuleSet(ruleSetNode, 100); + + assertEquals("Unexpected list of linking folders.", List.of(linkingFolder), linkingFolders); + } + + /** Check that permissions affect which linked folders are returned to the user. */ + @Test + public void testGetFoldersLinkingToRuleSet_respectsPermissions() + { + NodeRef ruleSetNode = new NodeRef("rule://set/"); + NodeRef owningFolder = new NodeRef("owning://folder/"); + ChildAssociationRef owningAssocMock = mock(ChildAssociationRef.class); + given(owningAssocMock.getParentRef()).willReturn(owningFolder); + given(nodeService.getPrimaryParent(ruleSetNode)).willReturn(owningAssocMock); + // Simulate a folder linking to the rule set. + NodeRef linkingFolder = new NodeRef("linking://folder/"); + ChildAssociationRef linkingAssocMock = mock(ChildAssociationRef.class); + given(linkingAssocMock.getParentRef()).willReturn(linkingFolder); + given(nodeService.getParentAssocs(ruleSetNode)).willReturn(List.of(owningAssocMock, linkingAssocMock)); + // The currect user does not have permission to view the folder. + given(permissionService.hasReadPermission(linkingFolder)).willReturn(DENIED); + + List linkingFolders = ruleService.getFoldersLinkingToRuleSet(ruleSetNode, 100); + + assertEquals("Unexpected list of linking folders.", emptyList(), linkingFolders); + } +} diff --git a/repository/src/test/java/org/alfresco/repo/template/UnsafeMethodsTest.java b/repository/src/test/java/org/alfresco/repo/template/UnsafeMethodsTest.java new file mode 100644 index 0000000000..3528c75b90 --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/template/UnsafeMethodsTest.java @@ -0,0 +1,78 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2022 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.repo.template; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Map; + +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.TemplateExceptionHandler; +import junit.framework.TestCase; + +public class UnsafeMethodsTest extends TestCase +{ + private static final String TEST_TEMPLATES_PACKAGE = "/org/alfresco/repo/template/templates/"; + private static final String ALLOWED_TEXT = ": ALLOWED"; + private static final String BLOCKED_TEXT = ": BLOCKED"; + private static final String EXPECTED_RESULT = "Freemarker Unsafe Methods Testing\n" + + "=================================\n" + + "java.lang.Thread.getId(): ALLOWED\n" + + "java.lang.Thread.interrupt(): BLOCKED\n" + + "java.lang.Thread.currentThread(): BLOCKED\n"; + + private final Configuration configuration = new Configuration(Configuration.VERSION_2_3_31); + + public void testUnsafeMethods() throws Exception + { + configuration.setClassForTemplateLoading(getClass(), TEST_TEMPLATES_PACKAGE); + configuration.setDefaultEncoding("UTF-8"); + configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + Template template = configuration.getTemplate("unsafemethods.ftl"); + + Thread currentThread = Thread.currentThread(); + Map model = Map.of( + "allowedText", ALLOWED_TEXT, + "blockedText", BLOCKED_TEXT, + "thread", currentThread); + + String result = applyTemplate(template, model); + + assertFalse(currentThread.isInterrupted()); + assertEquals(EXPECTED_RESULT, result); + } + + private String applyTemplate(Template template, Map inputModel ) throws TemplateException, IOException + { + try (StringWriter stringWriter = new StringWriter()) + { + template.process(inputModel, stringWriter); + return stringWriter.toString(); + } + } +} diff --git a/repository/src/test/java/org/alfresco/repo/thumbnail/ThumbnailServiceImplTest.java b/repository/src/test/java/org/alfresco/repo/thumbnail/ThumbnailServiceImplTest.java index eb40c6cf1c..7c18e6048d 100644 --- a/repository/src/test/java/org/alfresco/repo/thumbnail/ThumbnailServiceImplTest.java +++ b/repository/src/test/java/org/alfresco/repo/thumbnail/ThumbnailServiceImplTest.java @@ -495,6 +495,7 @@ public class ThumbnailServiceImplTest extends BaseAlfrescoSpringTest params.put("aspect-name", ContentModel.ASPECT_GEN_CLASSIFIABLE); Rule rule = new Rule(); rule.setRuleType(RuleType.INBOUND); + rule.setTitle("Rule name"); Action action = this.actionService.createAction(AddFeaturesActionExecuter.NAME, params); ActionCondition condition = this.actionService.createActionCondition(NoConditionEvaluator.NAME, null); action.addActionCondition(condition); diff --git a/repository/src/test/java/org/alfresco/repo/workflow/AbstractWorkflowServiceIntegrationTest.java b/repository/src/test/java/org/alfresco/repo/workflow/AbstractWorkflowServiceIntegrationTest.java index 29869837a5..e4de626df8 100644 --- a/repository/src/test/java/org/alfresco/repo/workflow/AbstractWorkflowServiceIntegrationTest.java +++ b/repository/src/test/java/org/alfresco/repo/workflow/AbstractWorkflowServiceIntegrationTest.java @@ -1247,7 +1247,15 @@ public abstract class AbstractWorkflowServiceIntegrationTest extends BaseSpringT WorkflowDefinition definition = deployment.getDefinition(); return definition; } - + + protected WorkflowDefinition deployDefinition(String resource, String name, boolean fullAccess) + { + InputStream input = getInputStream(resource); + WorkflowDeployment deployment = workflowService.deployDefinition(getEngine(), input, XML, name, fullAccess); + WorkflowDefinition definition = deployment.getDefinition(); + return definition; + } + protected abstract QName getAdhocProcessName(); diff --git a/repository/src/test/java/org/alfresco/repo/workflow/activiti/ActivitiWorkflowServiceIntegrationTest.java b/repository/src/test/java/org/alfresco/repo/workflow/activiti/ActivitiWorkflowServiceIntegrationTest.java index de77392398..b531b725ac 100644 --- a/repository/src/test/java/org/alfresco/repo/workflow/activiti/ActivitiWorkflowServiceIntegrationTest.java +++ b/repository/src/test/java/org/alfresco/repo/workflow/activiti/ActivitiWorkflowServiceIntegrationTest.java @@ -790,6 +790,45 @@ public class ActivitiWorkflowServiceIntegrationTest extends AbstractWorkflowServ assertNull("Workflow should not be deployed", workflowDef); } + /** + * + */ + @Test + public void testMNT21638_1() + { + WorkflowDefinition definition = deployDefinition("activiti/test-MNT21638-1.bpmn20.xml"); + + personManager.setUser(USER1); + + // Start the Workflow + try + { + WorkflowPath path = workflowService.startWorkflow(definition.getId(), null); + fail("Workflow should not have been executed"); + } + catch (Exception e) + { + // Do nothing + } + } + + /** + * + */ + @Test + public void testMNT21638_2() + { + WorkflowDefinition definition = deployDefinition("activiti/test-MNT21638-2.bpmn20.xml", "MNT21638", true); + + personManager.setUser(USER1); + + // Start the Workflow + WorkflowPath path = workflowService.startWorkflow(definition.getId(), null); + String instanceId = path.getInstance().getId(); + + assertNotNull(instanceId); + } + private NodeRef findWorkflowParent() { RepositoryLocation workflowLocation = (RepositoryLocation) diff --git a/repository/src/test/java/org/alfresco/transform/client/registry/LocalTransformServiceRegistryConfigTest.java b/repository/src/test/java/org/alfresco/transform/registry/LocalTransformServiceRegistryConfigTest.java similarity index 96% rename from repository/src/test/java/org/alfresco/transform/client/registry/LocalTransformServiceRegistryConfigTest.java rename to repository/src/test/java/org/alfresco/transform/registry/LocalTransformServiceRegistryConfigTest.java index 6e770d6ae2..dfdf045254 100644 --- a/repository/src/test/java/org/alfresco/transform/client/registry/LocalTransformServiceRegistryConfigTest.java +++ b/repository/src/test/java/org/alfresco/transform/registry/LocalTransformServiceRegistryConfigTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2021 Alfresco Software Limited + * Copyright (C) 2005 - 2022 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -23,7 +23,7 @@ * along with Alfresco. If not, see . * #L% */ -package org.alfresco.transform.client.registry; +package org.alfresco.transform.registry; import com.fasterxml.jackson.databind.ObjectMapper; import org.alfresco.repo.content.MimetypeMap; @@ -32,12 +32,12 @@ import org.alfresco.repo.content.transform.LocalPipelineTransform; import org.alfresco.repo.content.transform.LocalTransformImpl; import org.alfresco.repo.content.transform.LocalTransformServiceRegistry; import org.alfresco.repo.content.transform.TransformerDebug; -import org.alfresco.transform.client.model.config.SupportedSourceAndTarget; -import org.alfresco.transform.client.model.config.TransformConfig; -import org.alfresco.transform.client.model.config.TransformOption; -import org.alfresco.transform.client.model.config.TransformOptionGroup; -import org.alfresco.transform.client.model.config.TransformOptionValue; -import org.alfresco.transform.client.model.config.Transformer; +import org.alfresco.transform.config.SupportedSourceAndTarget; +import org.alfresco.transform.config.TransformConfig; +import org.alfresco.transform.config.TransformOption; +import org.alfresco.transform.config.TransformOptionGroup; +import org.alfresco.transform.config.TransformOptionValue; +import org.alfresco.transform.config.Transformer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.log4j.Level; @@ -72,7 +72,7 @@ import static org.junit.Assert.fail; /** * Testing LocalTransformServiceRegistry. */ -public class LocalTransformServiceRegistryConfigTest extends TransformRegistryTest +public class LocalTransformServiceRegistryConfigTest extends TransformRegistryModelTest { public static final String HARD_CODED_VALUE = "hard coded value"; @@ -242,19 +242,21 @@ public class LocalTransformServiceRegistryConfigTest extends TransformRegistryTe protected int getExpectedTransformsForTestJsonPipeline() { // imagemagick -// {"sourceMediaType": "image/jpeg", "targetMediaType": "image/jpeg"}, -// {"sourceMediaType": "image/jpeg", "targetMediaType": "image/png"}, -// {"sourceMediaType": "image/jpeg", "targetMediaType": "image/bmp"} +// {"sourceMediaType": "image/png", "targetMediaType": "image/jpeg"}, +// {"sourceMediaType": "image/png", "targetMediaType": "image/gif"}, +// {"sourceMediaType": "image/png", "targetMediaType": "image/png"}, +// {"sourceMediaType": "image/png", "targetMediaType": "image/tiff"} // pdfrendere // {"sourceMediaType": "application/pdf", "targetMediaType": "image/png" } // libreoffice // {"sourceMediaType": "application/vnd.ms-outlook", "targetMediaType": "application/pdf"} +// {"sourceMediaType": "application/msword", "targetMediaType": "application/pdf" }, // officeToImageViaPdf // {"sourceMediaType": "application/msword", "targetMediaType": "image/gif" }, // {"sourceMediaType": "application/msword", "targetMediaType": "image/jpeg"}, // {"sourceMediaType": "application/msword", "targetMediaType": "image/png" }, // {"sourceMediaType": "application/msword", "targetMediaType": "image/tiff"} - return 3 + 1 + 1 + 4; // 9 + return 4 + 1 + 2 + 4; // 11 } /** @@ -391,6 +393,23 @@ public class LocalTransformServiceRegistryConfigTest extends TransformRegistryTe combinedConfig.register((TransformServiceRegistryImpl)registry); } + // The super class uses org.junit.jupiter:junit-jupiter-engine:5.8.2 + // This cannot be used in the Alfresco repo as other tests in this class fail as they expect junit 4 + @Override + protected void assertSupported(String sourceMimetype, long sourceSizeInBytes, String targetMimetype, + Map actualOptions, String renditionName, String unsupportedMsg) + { + boolean supported = registry.isSupported(sourceMimetype, sourceSizeInBytes, targetMimetype, actualOptions, renditionName); + if (unsupportedMsg != null && !unsupportedMsg.isEmpty()) + { + assertFalse(supported); + } + else + { + assertTrue(supported); + } + } + @Test public void testJsonConfig() throws IOException { @@ -802,7 +821,7 @@ public class LocalTransformServiceRegistryConfigTest extends TransformRegistryTe public void testPipelineAndFailover() { retrieveLocalTransformList("alfresco/local-transform-service-config-pipeline-and-failover-test.json"); - registry.assertErrorLogged("Transformer .* cannot have pipeline and failover sections.*pipeline-and-failover.*"); + registry.assertErrorLogged("Transformer .* cannot have both pipeline and failover sections.*pipeline-and-failover.*"); } @Test diff --git a/repository/src/test/resources/activiti/test-MNT21638-1.bpmn20.xml b/repository/src/test/resources/activiti/test-MNT21638-1.bpmn20.xml new file mode 100644 index 0000000000..a76f556af2 --- /dev/null +++ b/repository/src/test/resources/activiti/test-MNT21638-1.bpmn20.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + var exec = new org.alfresco.util.exec.RuntimeExec(); + exec.setCommand(["/bin/ls"]); + var res = exec.execute(); + java.lang.System.err.println(res.getStdOut()); + + + + + + + admin + + + + + + + + + + \ No newline at end of file diff --git a/repository/src/test/resources/activiti/test-MNT21638-2.bpmn20.xml b/repository/src/test/resources/activiti/test-MNT21638-2.bpmn20.xml new file mode 100644 index 0000000000..8032641314 --- /dev/null +++ b/repository/src/test/resources/activiti/test-MNT21638-2.bpmn20.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + var exec = new org.alfresco.util.exec.RuntimeExec(); + exec.setCommand(["/bin/ls"]); + var res = exec.execute(); + java.lang.System.err.println(res.getStdOut()); + + + + + + + admin + + + + + + + + + + \ No newline at end of file diff --git a/repository/src/test/resources/alfresco/local-transform-service-config-pipeline-test.json b/repository/src/test/resources/alfresco/local-transform-service-config-pipeline-test.json index 488f0ed57e..0c782e3077 100644 --- a/repository/src/test/resources/alfresco/local-transform-service-config-pipeline-test.json +++ b/repository/src/test/resources/alfresco/local-transform-service-config-pipeline-test.json @@ -34,9 +34,10 @@ { "transformerName": "imagemagick", "supportedSourceAndTargetList": [ - {"sourceMediaType": "image/jpeg", "targetMediaType": "image/jpeg"}, - {"sourceMediaType": "image/jpeg", "targetMediaType": "image/png"}, - {"sourceMediaType": "image/jpeg", "targetMediaType": "image/bmp"} + {"sourceMediaType": "image/png", "targetMediaType": "image/jpeg"}, + {"sourceMediaType": "image/png", "targetMediaType": "image/gif"}, + {"sourceMediaType": "image/png", "targetMediaType": "image/png"}, + {"sourceMediaType": "image/png", "targetMediaType": "image/tiff"} ], "transformOptions": [ "imageMagickOptions" @@ -54,6 +55,7 @@ { "transformerName": "libreoffice", "supportedSourceAndTargetList": [ + {"sourceMediaType": "application/msword", "targetMediaType": "application/pdf" }, {"sourceMediaType": "application/vnd.ms-outlook", "targetMediaType": "application/pdf"} ] }, diff --git a/repository/src/test/resources/alfresco/transforms/0999-testPipeline.json b/repository/src/test/resources/alfresco/transforms/0999-testPipeline.json index 77b79c65fb..3cfc043860 100644 --- a/repository/src/test/resources/alfresco/transforms/0999-testPipeline.json +++ b/repository/src/test/resources/alfresco/transforms/0999-testPipeline.json @@ -1,16 +1,17 @@ { - "transformers": [ + "overrideSupported": [ { - "transformerName": "rubbish", - "transformerPipeline" : [ - {"transformerName": "appleIWorks", "targetMediaType": "image/jpeg"}, - {"transformerName": "imagemagick"} - ], - "supportedSourceAndTargetList": [ - {"sourceMediaType": "application/vnd.apple.pages", "maxSourceSizeBytes": 786432, "targetMediaType": "rubbish"} - ], - "transformOptions": [ - ] + "transformerName": "appleIWorks", + "sourceMediaType": "application/vnd.apple.pages", + "targetMediaType": "image/jpeg", + "maxSourceSizeBytes": 786432 + } + ], + "addSupported": [ + { + "transformerName": "imagemagick", + "sourceMediaType": "image/jpeg", + "targetMediaType": "rubbish" } ] } \ No newline at end of file diff --git a/repository/src/test/resources/org/alfresco/repo/preference/script/test_preferenceService2.js b/repository/src/test/resources/org/alfresco/repo/preference/script/test_preferenceService2.js new file mode 100644 index 0000000000..a4088e7cce --- /dev/null +++ b/repository/src/test/resources/org/alfresco/repo/preference/script/test_preferenceService2.js @@ -0,0 +1,11 @@ +function testPreferences() +{ + var preferences = { + "org.alfresco.share.forum.summary.dashlet.component-1-3.history": "1" + }; + + preferenceService.setPreferences(username, preferences); +} + +// Execute tests +testPreferences(); \ No newline at end of file diff --git a/repository/src/test/resources/org/alfresco/repo/template/templates/unsafemethods.ftl b/repository/src/test/resources/org/alfresco/repo/template/templates/unsafemethods.ftl new file mode 100644 index 0000000000..f10815e835 --- /dev/null +++ b/repository/src/test/resources/org/alfresco/repo/template/templates/unsafemethods.ftl @@ -0,0 +1,5 @@ +Freemarker Unsafe Methods Testing +================================= +java.lang.Thread.getId()<#if (thread.getId())??>${allowedText}<#else>${blockedText} +java.lang.Thread.interrupt()<#if (thread.interrupt())??>${allowedText}<#else>${blockedText} +java.lang.Thread.currentThread()<#if (thread.currentThread())??>${allowedText}<#else>${blockedText}