From 0c23a3fa4b5c7811182e2896c113a369cb7fbf93 Mon Sep 17 00:00:00 2001 From: Alex Mukha Date: Thu, 1 Aug 2019 22:34:53 +0100 Subject: [PATCH] Initial version after move --- packaging/tests/tas-cmis/.gitignore | 34 +- packaging/tests/tas-cmis/.travis.settings.xml | 10 + packaging/tests/tas-cmis/.travis.yml | 37 + packaging/tests/tas-cmis/CODE_OF_CONDUCT.md | 13 + packaging/tests/tas-cmis/README.md | 496 ++++++- packaging/tests/tas-cmis/docs/CHANGELOG.md | 20 + .../tas-cmis/docs/pics/html-report-sample.JPG | Bin 0 -> 56842 bytes .../tas-cmis/docs/pics/html-report-sample.png | Bin 0 -> 119323 bytes .../tas-cmis/docs/pics/xml-steps-report.JPG | Bin 0 -> 45913 bytes packaging/tests/tas-cmis/pom.xml | 171 +++ .../cmis/AuthParameterProviderFactory.java | 130 ++ .../org/alfresco/cmis/CmisProperties.java | 64 + .../java/org/alfresco/cmis/CmisWrapper.java | 1111 ++++++++++++++++ .../org/alfresco/cmis/dsl/BaseObjectType.java | 185 +++ .../java/org/alfresco/cmis/dsl/CheckIn.java | 78 ++ .../org/alfresco/cmis/dsl/CmisAssertion.java | 1153 +++++++++++++++++ .../java/org/alfresco/cmis/dsl/CmisUtil.java | 751 +++++++++++ .../alfresco/cmis/dsl/DocumentVersioning.java | 137 ++ .../java/org/alfresco/cmis/dsl/JmxUtil.java | 39 + .../org/alfresco/cmis/dsl/QueryExecutor.java | 226 ++++ .../exception/InvalidCmisObjectException.java | 10 + .../cmis/exception/UnrecognizedBinding.java | 12 + .../main/resources/alfresco-cmis-context.xml | 16 + .../src/main/resources/default.properties | 76 ++ .../src/main/resources/log4j.properties | 26 + .../shared-resources/cmis-runner-suite.xml | 62 + .../shared-resources/cmis-sanity-suite.xml | 25 + .../shared-resources/cmis-suites.xml | 25 + .../shared-resources/model/tas-model.xml | 151 +++ .../resources/shared-resources/testCount.xml | 18 + .../testdata/cmis-checkIn.txt | 1 + .../shared-resources/testdata/cmis-resource | 1 + 32 files changed, 5067 insertions(+), 11 deletions(-) create mode 100644 packaging/tests/tas-cmis/.travis.settings.xml create mode 100644 packaging/tests/tas-cmis/.travis.yml create mode 100644 packaging/tests/tas-cmis/CODE_OF_CONDUCT.md create mode 100644 packaging/tests/tas-cmis/docs/CHANGELOG.md create mode 100644 packaging/tests/tas-cmis/docs/pics/html-report-sample.JPG create mode 100644 packaging/tests/tas-cmis/docs/pics/html-report-sample.png create mode 100644 packaging/tests/tas-cmis/docs/pics/xml-steps-report.JPG create mode 100644 packaging/tests/tas-cmis/pom.xml create mode 100644 packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/AuthParameterProviderFactory.java create mode 100644 packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/CmisProperties.java create mode 100644 packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/CmisWrapper.java create mode 100644 packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/BaseObjectType.java create mode 100644 packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/CheckIn.java create mode 100644 packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/CmisAssertion.java create mode 100644 packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/CmisUtil.java create mode 100644 packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/DocumentVersioning.java create mode 100644 packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/JmxUtil.java create mode 100644 packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/QueryExecutor.java create mode 100644 packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/exception/InvalidCmisObjectException.java create mode 100644 packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/exception/UnrecognizedBinding.java create mode 100644 packaging/tests/tas-cmis/src/main/resources/alfresco-cmis-context.xml create mode 100644 packaging/tests/tas-cmis/src/main/resources/default.properties create mode 100644 packaging/tests/tas-cmis/src/main/resources/log4j.properties create mode 100644 packaging/tests/tas-cmis/src/main/resources/shared-resources/cmis-runner-suite.xml create mode 100644 packaging/tests/tas-cmis/src/main/resources/shared-resources/cmis-sanity-suite.xml create mode 100644 packaging/tests/tas-cmis/src/main/resources/shared-resources/cmis-suites.xml create mode 100644 packaging/tests/tas-cmis/src/main/resources/shared-resources/model/tas-model.xml create mode 100644 packaging/tests/tas-cmis/src/main/resources/shared-resources/testCount.xml create mode 100644 packaging/tests/tas-cmis/src/main/resources/shared-resources/testdata/cmis-checkIn.txt create mode 100644 packaging/tests/tas-cmis/src/main/resources/shared-resources/testdata/cmis-resource diff --git a/packaging/tests/tas-cmis/.gitignore b/packaging/tests/tas-cmis/.gitignore index a1c2a238a9..5bcc0634bd 100644 --- a/packaging/tests/tas-cmis/.gitignore +++ b/packaging/tests/tas-cmis/.gitignore @@ -1,23 +1,37 @@ -# Compiled class file *.class -# Log file -*.log +# Eclipse +.classpath +.settings +.project -# BlueJ files -*.ctxt +# Intellij +.idea/ +*.iml +*.iws + +# Mac +.DS_Store + +# Maven +target +*.log +*.log.* # Mobile Tools for Java (J2ME) -.mtj.tmp/ + +.mtj +.tmp/ # Package Files # + *.jar *.war -*.nar *.ear -*.zip -*.tar.gz -*.rar # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml + hs_err_pid* +alf_data +/src/main/resources/alfresco-global.properties +/src/main/resources/alfresco/extension/custom-log4j.properties diff --git a/packaging/tests/tas-cmis/.travis.settings.xml b/packaging/tests/tas-cmis/.travis.settings.xml new file mode 100644 index 0000000000..e8f1196935 --- /dev/null +++ b/packaging/tests/tas-cmis/.travis.settings.xml @@ -0,0 +1,10 @@ + + + + + alfresco-public + ${env.MAVEN_USERNAME} + ${env.MAVEN_PASSWORD} + + + diff --git a/packaging/tests/tas-cmis/.travis.yml b/packaging/tests/tas-cmis/.travis.yml new file mode 100644 index 0000000000..cd4f735fde --- /dev/null +++ b/packaging/tests/tas-cmis/.travis.yml @@ -0,0 +1,37 @@ +dist: trusty +sudo: required +language: java +jdk: + - openjdk11 + +cache: + directories: + - $HOME/.m2/repository + +branches: + only: + - master + +install: travis_retry mvn install -DskipTests=true -B -V + +stages: + - test + - release + +jobs: + include: + - stage: test + name: "Build and test" + script: travis_retry mvn test + - stage: release + name: "Push to Nexus" + if: fork = false AND branch = master AND type != pull_request AND commit_message !~ /\[no-release\]/ + before_install: + - "cp .travis.settings.xml $HOME/.m2/settings.xml" + script: + # Use full history for release + - git checkout -B "${TRAVIS_BRANCH}" + # Add email to link commits to user + - git config user.email "${GIT_EMAIL}" + # Skip building of release commits + - mvn --batch-mode -DscmCommentPrefix="[maven-release-plugin][skip ci] " -Dusername="${GIT_USERNAME}" -Dpassword="${GIT_PASSWORD}" -DskipTests -Darguments=-DskipTests release:clean release:prepare release:perform diff --git a/packaging/tests/tas-cmis/CODE_OF_CONDUCT.md b/packaging/tests/tas-cmis/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..2ff9dbad09 --- /dev/null +++ b/packaging/tests/tas-cmis/CODE_OF_CONDUCT.md @@ -0,0 +1,13 @@ +# Contributor Code of Conduct + +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 activities. + +We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. + +Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. + +This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) \ No newline at end of file diff --git a/packaging/tests/tas-cmis/README.md b/packaging/tests/tas-cmis/README.md index b6692266a6..aa099eab6a 100644 --- a/packaging/tests/tas-cmis/README.md +++ b/packaging/tests/tas-cmis/README.md @@ -1 +1,495 @@ -# alfresco-tas-cmis \ No newline at end of file +![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 0000000000000000000000000000000000000000..d03f6e2086a0be14504c83635d68abb8bcbb91aa GIT binary patch literal 56842 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..576422e34f3dd53fb1457ccda0f9f993dfccdac6 GIT binary patch literal 119323 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 PSA6mRsnX(fe`fzb1d(eu literal 0 HcmV?d00001 diff --git a/packaging/tests/tas-cmis/pom.xml b/packaging/tests/tas-cmis/pom.xml new file mode 100644 index 0000000000..814aac7edb --- /dev/null +++ b/packaging/tests/tas-cmis/pom.xml @@ -0,0 +1,171 @@ + + + 4.0.0 + org.alfresco.tas + cmis + alfresco-tas-cmis + 1.0-SNAPSHOT + + org.alfresco + alfresco-super-pom + 10 + + + + Alfresco Software + http://www.alfresco.com/ + + + + Paul Brodner + + Test Automation Architect + + + + + + src/main/resources/shared-resources/cmis-suites.xml + UTF-8 + 3.0.8 + 1.5 + 1.0.0 + 3.1.1 + 2.5.3 + 11 + + + + scm:git:https://github.com/Alfresco/alfresco-tas-cmis.git + scm:git:https://github.com/Alfresco/alfresco-tas-cmis.git + https://github.com/Alfresco/alfresco-tas-cmis + HEAD + + + + JIRA + https://issues.alfresco.com/jira/browse/ + + + + + alfresco-public + https://artifacts.alfresco.com/nexus/content/repositories/releases + + + alfresco-public + https://artifacts.alfresco.com/nexus/content/repositories/snapshots + + + + + + + alfresco-public + https://artifacts.alfresco.com/nexus/content/groups/public + + + + + + + maven-release-plugin + ${maven-release.version} + + v@{project.version} + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + test-jar + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + + org.apache.maven.plugins + maven-remote-resources-plugin + ${maven-remote-resources.version} + + + shared-resources/**/* + + + org.alfresco.tas:utility:${tas.utility.version} + + + + + + process + bundle + + + + + + + + + + org.jboss.resteasy + resteasy-jackson2-provider + 3.6.3.Final + + + + + org.alfresco.tas + utility + ${tas.utility.version} + + + + + org.apache.chemistry.opencmis + chemistry-opencmis-commons-api + ${chemistry-opencmis-commons-api} + + + + + + + org.apache.maven.plugins + maven-project-info-reports-plugin + 2.9 + + + + dependencies + issue-tracking + scm + + + + + + + + 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..7338000bbb --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/CmisWrapper.java @@ -0,0 +1,1111 @@ +package org.alfresco.cmis; + +import static org.alfresco.utility.Utility.checkObjectIsInitialized; +import static org.alfresco.utility.report.log.Step.STEP; + +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 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; + +@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()); + } + 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 | CmisRuntimeException re) + { + 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..8fd4267fd5 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/CmisAssertion.java @@ -0,0 +1,1153 @@ +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.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())); + 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 + { + Assert.assertFalse(false, 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..92a724c1df --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/CmisUtil.java @@ -0,0 +1,751 @@ +package org.alfresco.cmis.dsl; + +import static org.alfresco.utility.report.log.Step.STEP; + +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 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.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; + +/** + * 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 = false; + try + { + result = getPWCDocument().isPrivateWorkingCopy(); + } + 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) + { + List aspects = new ArrayList(); + aspects.add("P:cm:titled"); + Map properties = new HashMap(); + properties.put(PropertyIds.OBJECT_TYPE_ID, baseTypeId); + properties.put(PropertyIds.NAME, contentModel.getName()); + 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..55954e4eb0 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/java/org/alfresco/cmis/dsl/QueryExecutor.java @@ -0,0 +1,226 @@ +package org.alfresco.cmis.dsl; + +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.stream.Collectors; + +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.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 returnedResults = -1; + private String currentQuery = ""; + private ItemIterable results; + + public QueryExecutor(CmisWrapper cmisWrapper, String query) + { + this.cmisWrapper = cmisWrapper; + currentQuery = query; + } + + public QueryResultAssertion assertResultsCount() + { + returnedResults = executeQuery(currentQuery).getPageNumItems(); + return new QueryResultAssertion(); + } + + public QueryResultAssertion assertColumnIsOrdered() + { + results = executeQuery(currentQuery); + return new QueryResultAssertion(); + } + + public QueryResultAssertion assertColumnValuesRange() + { + results = executeQuery(currentQuery); + return new QueryResultAssertion(); + } + + 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 equals(long expectedValue) + { + STEP(String.format("Verify that query: '%s' has %d results count returned", currentQuery, expectedValue)); + Assert.assertEquals(returnedResults, 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 <= returnedResults) + Assert.fail(String.format("%s expected to have more than %d results, but found %d", showErrorMessage(), expectedValue, returnedResults)); + + return this; + } + + public QueryResultAssertion isLowerThan(long expectedValue) + { + STEP(String.format("Verify that query: '%s' has more than %d results count returned", currentQuery, expectedValue)); + if (returnedResults >= expectedValue) + Assert.fail(String.format("%s expected to have less than %d results, but found %d", showErrorMessage(), expectedValue, returnedResults)); + + 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(Collectors.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(Collectors.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; + + } + + 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/main/resources/shared-resources/cmis-runner-suite.xml b/packaging/tests/tas-cmis/src/main/resources/shared-resources/cmis-runner-suite.xml new file mode 100644 index 0000000000..08d75ef621 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/resources/shared-resources/cmis-runner-suite.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packaging/tests/tas-cmis/src/main/resources/shared-resources/cmis-sanity-suite.xml b/packaging/tests/tas-cmis/src/main/resources/shared-resources/cmis-sanity-suite.xml new file mode 100644 index 0000000000..38fdd63e74 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/resources/shared-resources/cmis-sanity-suite.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packaging/tests/tas-cmis/src/main/resources/shared-resources/cmis-suites.xml b/packaging/tests/tas-cmis/src/main/resources/shared-resources/cmis-suites.xml new file mode 100644 index 0000000000..9963b115e8 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/resources/shared-resources/cmis-suites.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packaging/tests/tas-cmis/src/main/resources/shared-resources/model/tas-model.xml b/packaging/tests/tas-cmis/src/main/resources/shared-resources/model/tas-model.xml new file mode 100644 index 0000000000..892027f9c1 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/resources/shared-resources/model/tas-model.xml @@ -0,0 +1,151 @@ + + + + + Alfresco TAS custom model + Bogdan + 1.0 + + + + + + + + + + + + + CMIS TAS Content + cm:content + + + Text + d:text + + + Datetime + d:datetime + + + Integer + d:int + + + Long + d:long + + + Multiply String + d:text + true + true + + + + + + + false + false + + + cm:content + false + false + + + + + + + CMIS TAS Folder + cm:folder + + + Text + d:text + + + Datetime + d:datetime + + + Integer + d:int + + + Multiply String + d:text + true + true + + + + + + + false + false + + + cm:folder + false + false + + + + + + + + + TAS Content Aspect + + + Aspect Text + d:text + + + Aspect Datetime + d:datetime + + + Aspect Integer + d:int + + + Aspect Multiply String + d:text + true + true + + + + + + TAS Folder Aspect + + + Aspect Text + d:text + + + Aspect Datetime + d:datetime + + + Aspect Integer + d:int + + + Aspect Multiply String + d:text + true + true + + + + + \ No newline at end of file diff --git a/packaging/tests/tas-cmis/src/main/resources/shared-resources/testCount.xml b/packaging/tests/tas-cmis/src/main/resources/shared-resources/testCount.xml new file mode 100644 index 0000000000..249e06ac86 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/resources/shared-resources/testCount.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packaging/tests/tas-cmis/src/main/resources/shared-resources/testdata/cmis-checkIn.txt b/packaging/tests/tas-cmis/src/main/resources/shared-resources/testdata/cmis-checkIn.txt new file mode 100644 index 0000000000..863a223b69 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/resources/shared-resources/testdata/cmis-checkIn.txt @@ -0,0 +1 @@ +Sp23xfcYhUBYpsXuPFzn8nVQ \ No newline at end of file diff --git a/packaging/tests/tas-cmis/src/main/resources/shared-resources/testdata/cmis-resource b/packaging/tests/tas-cmis/src/main/resources/shared-resources/testdata/cmis-resource new file mode 100644 index 0000000000..e01d809153 --- /dev/null +++ b/packaging/tests/tas-cmis/src/main/resources/shared-resources/testdata/cmis-resource @@ -0,0 +1 @@ +tas \ No newline at end of file