From 3dc624a652236bd4161666df93f327a7315bf773 Mon Sep 17 00:00:00 2001 From: Satyam Sah Date: Thu, 2 Jul 2026 11:34:03 +0530 Subject: [PATCH] ACS-11435 Elasticsearch TAS Tests (#1682) * ACS-11434 testing in community packaging * ACS-11434 minor fixes * ACS-11434 added tag indexing tests * ACS-11434 refactored tests * ACS-11434 precommit fix * ACS-11434 addressed copilot comments * ACS-11434 addressed pr comments * ACS-11434 Update batch indexing image to latest alpha * ACS-11434 activate all-tas-tests profile while build * ACS-11434 fix ci build * ACS-11434 fix ci build * ACS-11434 fix ci build * ACS-11434 Sync with master * ACS-11434 fix ci * ACS-11434 add missing env parameters * ACS-11434 ci fix * ACS-11434 trim whitespace from batch indexing tag * ACS-11434 Added alfresco with jdbc jars config * ACS-11434 Fix basic auth tests and remove testrail * ACS-11434 Fix precommit * ACS-11434 Added retry listeners for basic auth test suite * ACS-11434 Fix opensearch basicauth tests --- .github/workflows/ci.yml | 74 +++ scripts/ci/build.sh | 10 + tests/environment/.env | 10 +- .../alfresco.Dockerfile | 6 + tests/pom.xml | 2 + tests/tas-elasticsearch/pom.xml | 135 ++++ .../elasticsearch/CategoryIndexingTests.java | 201 ++++++ .../ElasticsearchBoostedSearchTests.java | 395 ++++++++++++ .../ElasticsearchCMISPathTests.java | 145 +++++ .../elasticsearch/ElasticsearchCMISTests.java | 604 ++++++++++++++++++ .../ElasticsearchCategoriesCountTests.java | 311 +++++++++ .../ElasticsearchGetTagsTests.java | 250 ++++++++ .../ElasticsearchIsUnsetTest.java | 266 ++++++++ .../ElasticsearchLimitTests.java | 139 ++++ .../ElasticsearchLiveIndexingTests.java | 263 ++++++++ .../ElasticsearchPathIndexingTests.java | 301 +++++++++ .../ElasticsearchPlainHighlightingTests.java | 478 ++++++++++++++ .../ElasticsearchProximitySearchTests.java | 253 ++++++++ .../ElasticsearchSiteIndexingTests.java | 522 +++++++++++++++ .../ElasticsearchTagIndexingTests.java | 279 ++++++++ .../ElasticsearchTemplateSearchTests.java | 316 +++++++++ .../ElasticsearchTokenisationTests.java | 198 ++++++ .../NodeWithCategoryIndexingTests.java | 192 ++++++ .../NodesSecondaryAncestorIndexingTests.java | 368 +++++++++++ .../NodesSecondaryChildrenRelatedTests.java | 90 +++ .../NodesSecondaryParentIndexingTests.java | 361 +++++++++++ .../NodesSecondaryPathIndexingTests.java | 392 ++++++++++++ .../PathFieldsIndexingTests.java | 174 +++++ .../elasticsearch/PathUpdateTests.java | 271 ++++++++ .../elasticsearch/SearchQueryService.java | 286 +++++++++ .../AlfrescoStackInitializerESBasicAuth.java | 178 ++++++ .../ElasticsearchBasicAuthTests.java | 91 +++ .../elasticsearch/retry/RetryAnalyzer.java | 63 ++ .../retry/RetryAnnotationTransformer.java | 44 ++ .../utility/ElasticsearchRESTHelper.java | 166 +++++ .../alfresco-elasticsearch-context.xml | 16 + .../test/resources/exactTermSearch.properties | 5 + .../src/test/resources/log4j2.properties | 16 + .../resources/test-data/alfresco-logo.png | Bin 0 -> 6059 bytes .../elasticsearch-basic-auth-suite.xml | 12 + .../test-suites/elasticsearch-suite.xml | 12 + .../standard-elasticsearch-suites.xml | 10 + .../test/resources/testcontainers.properties | 2 + tests/testcontainers-env/pom.xml | 68 ++ .../tas/AlfrescoStackInitializer.java | 521 +++++++++++++++ .../java/org/alfresco/tas/DatabaseType.java | 100 +++ .../main/java/org/alfresco/tas/EnvHelper.java | 73 +++ .../org/alfresco/tas/MavenPropertyHelper.java | 83 +++ .../org/alfresco/tas/SearchEngineType.java | 53 ++ .../alfresco/tas/SystemPropertyHelper.java | 53 ++ .../org/alfresco/tas/TestDataUtility.java | 46 ++ .../src/main/resources/maven.properties | 5 + 52 files changed, 8907 insertions(+), 2 deletions(-) create mode 100644 tests/environment/alfresco-with-jdbc-drivers/alfresco.Dockerfile create mode 100644 tests/tas-elasticsearch/pom.xml create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/CategoryIndexingTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchBoostedSearchTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchCMISPathTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchCMISTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchCategoriesCountTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchGetTagsTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchIsUnsetTest.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchLimitTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchLiveIndexingTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchPathIndexingTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchPlainHighlightingTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchProximitySearchTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchSiteIndexingTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchTagIndexingTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchTemplateSearchTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchTokenisationTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodeWithCategoryIndexingTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodesSecondaryAncestorIndexingTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodesSecondaryChildrenRelatedTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodesSecondaryParentIndexingTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodesSecondaryPathIndexingTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/PathFieldsIndexingTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/PathUpdateTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/SearchQueryService.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/basicAuth/AlfrescoStackInitializerESBasicAuth.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/basicAuth/ElasticsearchBasicAuthTests.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/retry/RetryAnalyzer.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/retry/RetryAnnotationTransformer.java create mode 100644 tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/utility/ElasticsearchRESTHelper.java create mode 100644 tests/tas-elasticsearch/src/test/resources/alfresco-elasticsearch-context.xml create mode 100644 tests/tas-elasticsearch/src/test/resources/exactTermSearch.properties create mode 100644 tests/tas-elasticsearch/src/test/resources/log4j2.properties create mode 100644 tests/tas-elasticsearch/src/test/resources/test-data/alfresco-logo.png create mode 100644 tests/tas-elasticsearch/src/test/resources/test-suites/elasticsearch-basic-auth-suite.xml create mode 100644 tests/tas-elasticsearch/src/test/resources/test-suites/elasticsearch-suite.xml create mode 100644 tests/tas-elasticsearch/src/test/resources/test-suites/standard-elasticsearch-suites.xml create mode 100644 tests/tas-elasticsearch/src/test/resources/testcontainers.properties create mode 100644 tests/testcontainers-env/pom.xml create mode 100644 tests/testcontainers-env/src/main/java/org/alfresco/tas/AlfrescoStackInitializer.java create mode 100644 tests/testcontainers-env/src/main/java/org/alfresco/tas/DatabaseType.java create mode 100644 tests/testcontainers-env/src/main/java/org/alfresco/tas/EnvHelper.java create mode 100644 tests/testcontainers-env/src/main/java/org/alfresco/tas/MavenPropertyHelper.java create mode 100644 tests/testcontainers-env/src/main/java/org/alfresco/tas/SearchEngineType.java create mode 100644 tests/testcontainers-env/src/main/java/org/alfresco/tas/SystemPropertyHelper.java create mode 100644 tests/testcontainers-env/src/main/java/org/alfresco/tas/TestDataUtility.java create mode 100644 tests/testcontainers-env/src/main/resources/maven.properties diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd7221010..98c2e7abe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,6 +112,80 @@ jobs: - name: "Clean Maven cache" run: bash ./scripts/ci/cleanup_cache.sh + tas_tests_search_api: + name: ${{ matrix.testSuite }} | TAS tests (Search API) + runs-on: ubuntu-latest + if: > + (github.ref_name == 'master' || startsWith(github.ref_name, 'release/') || github.event_name == 'pull_request') && + !contains(github.event.head_commit.message, '[skip search]') && + !contains(github.event.head_commit.message, '[skip tests]') + strategy: + fail-fast: false + matrix: + include: + - testSuite: Elasticsearch postgreSQL + profiles: all-tas-tests,elastic + search-engine-type: elasticsearch + db-type: postgresql + - testSuite: Elasticsearch MySQL + profiles: all-tas-tests,elastic + search-engine-type: elasticsearch + db-type: mysql + - testSuite: Elasticsearch Maria DB 11.8 + profiles: all-tas-tests,elastic + search-engine-type: elasticsearch + db-type: mariadb + - testSuite: Opensearch + profiles: all-tas-tests,elastic + search-engine-type: opensearch + db-type: postgresql + - testSuite: Elasticsearch Basic Auth postgreSQL + profiles: all-tas-tests,elastic-basic-auth + search-engine-type: elasticsearch + db-type: postgresql + - testSuite: Elasticsearch Basic Auth MySQL + profiles: all-tas-tests,elastic-basic-auth + search-engine-type: elasticsearch + db-type: mysql + - testSuite: Elasticsearch Basic Auth MariaDB 11.8 + profiles: all-tas-tests,elastic-basic-auth + search-engine-type: elasticsearch + db-type: mariadb + - testSuite: Opensearch Basic Auth + profiles: all-tas-tests,elastic-basic-auth + search-engine-type: opensearch + db-type: postgresql + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v12.4.3 + - uses: Alfresco/alfresco-build-tools/.github/actions/setup-java-build@v12.4.3 + with: + java-version: ${{ env.JAVA_VERSION }} + - name: "Build" + timeout-minutes: ${{ fromJSON(env.GITHUB_ACTIONS_DEPLOY_TIMEOUT) }} + run: | + bash ./scripts/ci/init.sh + bash ./scripts/ci/build.sh + - name: "Run tests" + id: tests + timeout-minutes: ${{ fromJSON(env.GITHUB_ACTIONS_DEPLOY_TIMEOUT) }} + run: | + set -a + source ${TAS_ENVIRONMENT}/.env + set +a + BATCH_INDEXING_TAG="${BATCH_INDEXING_TAG//[[:space:]]/}" + mvn -B -ntp -q install -N -f tests/pom.xml + mvn -B -ntp -q install -DskipTests -f tests/testcontainers-env/pom.xml + mvn -B install -ntp -f tests/tas-elasticsearch/pom.xml -P${{ matrix.profiles }} -Denvironment=default -DrunBugs=false "-Dsearch.engine.type=${{ matrix.search-engine-type }}" "-Ddatabase.type=${{ matrix.db-type }}" -Dindeximage="alfresco/alfresco-elasticsearch-batch-indexing:${BATCH_INDEXING_TAG}" -Drepoimage="alfresco-repository-databases:latest" + - name: "Dump all Docker containers logs" + uses: Alfresco/alfresco-build-tools/.github/actions/docker-dump-containers-logs@v12.4.3 + if: failure() && steps.tests.outcome == 'failure' + - name: "Clean Maven cache" + run: bash ./scripts/ci/cleanup_cache.sh + + community_zip_tests: name: Community Distribution Zip content tests runs-on: ubuntu-latest diff --git a/scripts/ci/build.sh b/scripts/ci/build.sh index 693413040..21cd4c1cc 100755 --- a/scripts/ci/build.sh +++ b/scripts/ci/build.sh @@ -85,6 +85,16 @@ fi # Build the current project mvn -B -ntp -V -q install -DskipTests -Dmaven.javadoc.skip=true -P$BUILD_PROFILE -Pags ${REPO_IMAGE} ${SHARE_IMAGE} +# Build alfresco image with jdbc drivers (used by search API tests for MariaDB/MySQL) +MYSQL_JDBC_TAG=$(mvn help:evaluate -Dexpression=dependency.mysql.version -q -DforceStdout) +mvn dependency:copy -Dartifact=mysql:mysql-connector-java:${MYSQL_JDBC_TAG}:jar -DoutputDirectory=tests/environment/alfresco-with-jdbc-drivers + +MARIADB_JDBC_TAG=$(mvn help:evaluate -Dexpression=dependency.mariadb.version -q -DforceStdout) +mvn dependency:copy -Dartifact=org.mariadb.jdbc:mariadb-java-client:${MARIADB_JDBC_TAG}:jar -DoutputDirectory=tests/environment/alfresco-with-jdbc-drivers + +REPO_LATEST_IMAGE=$(docker images --format='{{.Repository}}:{{.Tag}}' | grep "alfresco-content-repository-community:latest") +docker build -t alfresco-repository-databases:latest -f tests/environment/alfresco-with-jdbc-drivers/alfresco.Dockerfile . --build-arg BASE_IMAGE=${REPO_LATEST_IMAGE} + popd set +vex diff --git a/tests/environment/.env b/tests/environment/.env index 2136c47b9..c9548d5b6 100644 --- a/tests/environment/.env +++ b/tests/environment/.env @@ -1,3 +1,9 @@ SOLR6_TAG=2.0.20 -POSTGRES_TAG=15.4 -ACTIVEMQ_TAG=5.18.3-jre17-rockylinux8 +ACTIVEMQ_TAG=6.2.0-jre17-rockylinux8 +ELASTICSEARCH_TAG=8.17.0 +BATCH_INDEXING_TAG=5.7.0-A.3 +POSTGRES_TAG=17.9 +MYSQL_TAG=8.0.30 +MARIADB_TAG=11.8 +OPENSEARCH_TAG=2.11.1 +OPENSEARCH_DASHBOARDS_TAG=2.11.1 diff --git a/tests/environment/alfresco-with-jdbc-drivers/alfresco.Dockerfile b/tests/environment/alfresco-with-jdbc-drivers/alfresco.Dockerfile new file mode 100644 index 000000000..1c8c02b6f --- /dev/null +++ b/tests/environment/alfresco-with-jdbc-drivers/alfresco.Dockerfile @@ -0,0 +1,6 @@ +#BUILDING CONTAINER FOR TAS TESTING WITH DIFFERENT JDBC CONNECTORS +ARG BASE_IMAGE +FROM $BASE_IMAGE + +COPY tests/environment/alfresco-with-jdbc-drivers/*.jar /usr/local/tomcat/lib/ + diff --git a/tests/pom.xml b/tests/pom.xml index f4a775f44..ba99d9877 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -10,7 +10,9 @@ + testcontainers-env tas-restapi + tas-elasticsearch tas-cmis tas-email tas-webdav diff --git a/tests/tas-elasticsearch/pom.xml b/tests/tas-elasticsearch/pom.xml new file mode 100644 index 000000000..de3ef3d16 --- /dev/null +++ b/tests/tas-elasticsearch/pom.xml @@ -0,0 +1,135 @@ + + + 4.0.0 + org.alfresco.tas + content-repository-community-elasticsearch-test + Elasticsearch test + jar + + + org.alfresco + content-repository-community-tests + 26.2.0-A.24-SNAPSHOT + + + + + elastic + + ${project.basedir}/src/test/resources/test-suites/standard-elasticsearch-suites.xml + + + + + + elastic-basic-auth + + ${project.basedir}/src/test/resources/test-suites/elasticsearch-basic-auth-suite.xml + + + + + + + + + org.alfresco.tas + content-repository-community-testcontainers + ${project.version} + test + + + org.mariadb.jdbc + mariadb-java-client + test + + + mysql + mysql-connector-java + test + + + + + org.alfresco.tas + restapi + tests + test + + + org.alfresco.tas + restapi + test + + + io.rest-assured + rest-assured + test + + + org.eclipse.parsson + parsson + test + + + org.springframework + spring-test + test + + + org.opensearch.client + opensearch-java + ${dependency.opensearch.version} + test + + + org.eclipse + yasson + + + + + org.jsoup + jsoup + test + + + org.assertj + assertj-core + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + + ${suiteXmlFile} + + + + --illegal-access=warn + --add-opens=java.base/java.lang=ALL-UNNAMED + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + + diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/CategoryIndexingTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/CategoryIndexingTests.java new file mode 100644 index 000000000..8e0aabf75 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/CategoryIndexingTests.java @@ -0,0 +1,201 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.alfresco.elasticsearch.SearchQueryService.req; +import static org.alfresco.utility.model.FileType.TEXT_PLAIN; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.elasticsearch.utility.ElasticsearchRESTHelper; +import org.alfresco.rest.core.RestWrapper; +import org.alfresco.rest.model.RestCategoryLinkBodyModel; +import org.alfresco.rest.model.RestCategoryModel; +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.data.RandomData; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; +import org.alfresco.utility.report.log.Step; + +/** + * Tests to verify batch indexing of category using Elasticsearch. + */ +@SuppressWarnings({"PMD.JUnitTestsShouldIncludeAssert"}) +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +public class CategoryIndexingTests extends AbstractTestNGSpringContextTests +{ + private static final String TEST_PREFIX = RandomData.getRandomAlphanumeric() + "_"; + private static final String CATEGORY_A_NAME = TEST_PREFIX + "CategoryA"; + private static final String CATEGORY_B_NAME = TEST_PREFIX + "CategoryB"; + private static final String ROOT_CATEGORY_ID = "-root-"; + private static final RestCategoryModel ROOT_CATEGORY = RestCategoryModel.builder().id(ROOT_CATEGORY_ID).create(); + + @Autowired + private ElasticsearchRESTHelper helper; + @Autowired + private ServerHealth serverHealth; + @Autowired + private DataUser dataUser; + @Autowired + private DataSite dataSite; + @Autowired + private DataContent dataContent; + @Autowired + protected SearchQueryService searchQueryService; + @Autowired + protected RestWrapper restClient; + + private RestCategoryModel categoryA; + private RestCategoryModel categoryB; + private UserModel testUser; + private SiteModel testSite; + private FileModel testFile; + private FolderModel testFolder; + + /** Create a user, private site and two categories. Create a folder (in category B) containing a document (in category A and category B). */ + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + serverHealth.isServerReachable(); + serverHealth.assertServerIsOnline(); + + Step.STEP("Create two categories under the root."); + List categories = List.of(RestCategoryModel.builder().name(CATEGORY_A_NAME).create(), + RestCategoryModel.builder().name(CATEGORY_B_NAME).create()); + List categoryList = restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI() + .usingCategory(ROOT_CATEGORY).createCategoriesList(categories).getEntries(); + categoryA = categoryList.get(0).onModel(); + categoryB = categoryList.get(1).onModel(); + + Step.STEP("Create a test user and use them to create a private site with a folder containing a document."); + testUser = dataUser.createRandomTestUser(); + testSite = dataSite.usingUser(testUser).createPrivateRandomSite(); + FolderModel folderModel = FolderModel.getRandomFolderModel(); + testFolder = dataContent.usingUser(testUser).usingSite(testSite).createFolder(folderModel); + FileModel fileModel = FileModel.getRandomFileModel(TEXT_PLAIN); + testFile = dataContent.usingUser(testUser).usingResource(testFolder).createContent(fileModel); + + Step.STEP("Assign the document to both categories and the folder to category B."); + RestCategoryLinkBodyModel categoryALink = RestCategoryLinkBodyModel.builder().categoryId(categoryA.getId()).create(); + RestCategoryLinkBodyModel categoryBLink = RestCategoryLinkBodyModel.builder().categoryId(categoryB.getId()).create(); + restClient.authenticateUser(testUser).withCoreAPI().usingNode(testFile).linkToCategory(categoryALink); + restClient.authenticateUser(testUser).withCoreAPI().usingNode(testFile).linkToCategory(categoryBLink); + restClient.authenticateUser(testUser).withCoreAPI().usingNode(testFolder).linkToCategory(categoryBLink); + } + + /** Check we can find the document assigned to a category. */ + @Test(groups = TestGroup.SEARCH) + public void testFindDocumentByCategory() + { + SearchRequest query = req("cm:categories:\"" + categoryA.getId() + "\""); + searchQueryService.expectResultsFromQuery(query, testUser, testFile.getName()); + } + + /** Check we can find the document assigned to a category even when the query includes a StoreRef. */ + @Test(groups = TestGroup.SEARCH) + public void testFindDocumentByCategoryWithStoreRef() + { + SearchRequest query = req("cm:categories:\"workspace://SpacesStore/" + categoryA.getId() + "\""); + searchQueryService.expectResultsFromQuery(query, testUser, testFile.getName()); + } + + /** Check we can find the folder and document assigned to the other category. */ + @Test(groups = TestGroup.SEARCH) + public void testFindFolderByCategory() + { + SearchRequest query = req("cm:categories:\"" + categoryB.getId() + "\""); + searchQueryService.expectResultsFromQuery(query, testUser, testFile.getName(), testFolder.getName()); + } + + /** Check we can find the document by the pseudo-path created for the category. */ + @Test(groups = TestGroup.SEARCH) + public void testQueryByCategoryPseudoPath() + { + SearchRequest query = req("PATH:\"/cm:categoryRoot/cm:generalclassifiable/cm:" + CATEGORY_A_NAME + "/*\""); + searchQueryService.expectResultsFromQuery(query, testUser, testFile.getName()); + } + + /** Check we can find the document by a partial path match for the category. */ + @Test(groups = TestGroup.SEARCH) + public void testQueryByPartialCategoryPathA() + { + SearchRequest query = req("PATH:\"//cm:" + CATEGORY_A_NAME + "/*\""); + searchQueryService.expectResultsFromQuery(query, testUser, testFile.getName()); + } + + /** Check we can find the document and folder by a partial path match for the second category. */ + @Test(groups = TestGroup.SEARCH) + public void testQueryByPartialCategoryPathB() + { + SearchRequest query = req("PATH:\"//cm:" + CATEGORY_B_NAME + "/*\""); + searchQueryService.expectResultsFromQuery(query, testUser, testFile.getName(), testFolder.getName()); + } + + /** Check we cannot find the document by a partial path match for the category that has been applied to it and then deleted. */ + @Test(groups = TestGroup.SEARCH) + public void testQueryByPathOnDeletedCategory() throws InterruptedException + { + // create 2 categories + final RestCategoryModel categoryToDelete = helper.createCategory(); + final RestCategoryModel otherCategory = helper.createCategory(); + + // assign both categories to a document + helper.linkToCategory(testUser, testFile, categoryToDelete); + helper.linkToCategory(testUser, testFile, otherCategory); + + // we can find the document by partial category paths after it is linked to both categories + SearchRequest categoryToDeleteQuery = req("PATH:\"//cm:" + categoryToDelete.getName() + "/*\""); + searchQueryService.expectResultsFromQuery(categoryToDeleteQuery, testUser, testFile.getName()); + SearchRequest otherCategoryQuery = req("PATH:\"//cm:" + otherCategory.getName() + "/*\""); + searchQueryService.expectResultsFromQuery(otherCategoryQuery, testUser, testFile.getName()); + + // delete one of linked categories + restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI() + .usingCategory(categoryToDelete).deleteCategory(); + restClient.assertStatusCodeIs(HttpStatus.NO_CONTENT); + + // we cannot find the document by partial category path anymore after the category is deleted + searchQueryService.expectNoResultsFromQuery(categoryToDeleteQuery, testUser); + // we can still find the document by partial category path with a different category that has been assigned but not deleted + searchQueryService.expectResultsFromQuery(otherCategoryQuery, testUser, testFile.getName()); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchBoostedSearchTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchBoostedSearchTests.java new file mode 100644 index 000000000..adeee4a86 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchBoostedSearchTests.java @@ -0,0 +1,395 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.apache.commons.lang3.StringUtils.EMPTY; + +import static org.alfresco.elasticsearch.SearchQueryService.req; +import static org.alfresco.utility.data.RandomData.getRandomFile; +import static org.alfresco.utility.data.RandomData.getRandomName; +import static org.alfresco.utility.report.log.Step.STEP; + +import java.time.Clock; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.model.RestTagModel; +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.model.ContentModel; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; + +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +public class ElasticsearchBoostedSearchTests extends AbstractTestNGSpringContextTests +{ + private static final String SEARCH_TERM = "mountain"; + private static final String DIFFERENT_SEARCH_TERM = "fountain"; + + @Autowired + private ServerHealth serverHealth; + + @Autowired + private DataUser dataUser; + + @Autowired + private DataContent dataContent; + + @Autowired + private SearchQueryService searchQueryService; + + private UserModel testUser; + private ContentModel fileWithTermInName; + private ContentModel fileWithDifferentTermInName; + private ContentModel fileWithPhraseInContent; + private ContentModel fileWithTermInTitle; + private ContentModel folderWithTermInName; + private ContentModel folderWithTermInTitle; + private ContentModel testFolder; + private LocalDateTime creationTime; + private LocalDateTime afterCreationTime; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + serverHealth.assertServerIsOnline(); + + STEP("Create a test user and few files and folders containing searched term in name, title and content"); + testUser = dataUser.createRandomTestUser(); + testFolder = createFolder(getRandomName("folder")); + fileWithTermInName = createFile(testFolder, SEARCH_TERM + ".txt", "dummy content"); + fileWithDifferentTermInName = createFile(testFolder, DIFFERENT_SEARCH_TERM + ".txt", "dummy other content"); + fileWithPhraseInContent = createFile(testFolder, getRandomFile(FileType.TEXT_PLAIN), "content with " + SEARCH_TERM + " searched phrase"); + folderWithTermInName = createFolder(testFolder, SEARCH_TERM); + creationTime = ZonedDateTime.now(Clock.system(ZoneOffset.UTC)).toLocalDateTime(); + fileWithTermInTitle = createRandomFileWithTitle(testFolder, SEARCH_TERM); + folderWithTermInTitle = createRandomFolderWithTitle(testFolder, SEARCH_TERM); + afterCreationTime = ZonedDateTime.now(Clock.system(ZoneOffset.UTC)).toLocalDateTime(); + } + + @AfterClass + public void dataCleanup() + { + STEP("Clean up created files, folders and user"); + dataContent.usingAdmin().usingResource(testFolder).deleteContent(); + dataUser.deleteUser(testUser); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_simpleTermBoost() + { + STEP("Search for files and folders by name with higher priority for files"); + String boostedQuery = "TYPE:('cm:content'^2 OR 'cm:folder'^0.5) AND cm:name:" + SEARCH_TERM; + SearchRequest searchRequest = req("afts", boostedQuery); + searchQueryService.expectResultsInOrder(searchRequest, testUser, fileWithTermInName.getName(), folderWithTermInName.getName()); + + STEP("Search for files and folders by name with higher priority for folders"); + String invertedBoost = "TYPE:('cm:content'^0.5 OR 'cm:folder'^2) AND cm:name:" + SEARCH_TERM; + searchRequest = req("afts", invertedBoost); + searchQueryService.expectResultsInOrder(searchRequest, testUser, folderWithTermInName.getName(), fileWithTermInName.getName()); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_complexTermBoost() + { + STEP("Search for files and folders by name or title with higher priority for files by name"); + String boostedQuery1 = "TYPE:('cm:content'^4 OR 'cm:folder'^0.5)^6 AND (cm:name:" + SEARCH_TERM + "^1.5 OR cm:title:" + SEARCH_TERM + "^0.5)"; + SearchRequest searchRequest = req("afts", boostedQuery1); + searchQueryService.expectResultsInOrder(searchRequest, testUser, fileWithTermInName.getName(), fileWithTermInTitle.getName(), folderWithTermInName.getName(), folderWithTermInTitle.getName()); + + STEP("Search for files and folders by name or title with higher priority for folders by name"); + String boostedQuery2 = "TYPE:('cm:content'^0.5 OR 'cm:folder'^4)^6 AND (cm:name:" + SEARCH_TERM + "^1.5 OR cm:title:" + SEARCH_TERM + "^0.5)"; + searchRequest = req("afts", boostedQuery2); + searchQueryService.expectResultsInOrder(searchRequest, testUser, folderWithTermInName.getName(), folderWithTermInTitle.getName(), fileWithTermInName.getName(), fileWithTermInTitle.getName()); + + STEP("Search for files and folders by name or title with higher priority for files by title"); + String boostedQuery3 = "TYPE:('cm:content'^4 OR 'cm:folder'^0.5)^6 AND (cm:name:" + SEARCH_TERM + "^0.5 OR cm:title:" + SEARCH_TERM + "^1.5)"; + searchRequest = req("afts", boostedQuery3); + searchQueryService.expectResultsInOrder(searchRequest, testUser, fileWithTermInTitle.getName(), fileWithTermInName.getName(), folderWithTermInTitle.getName(), folderWithTermInName.getName()); + + STEP("Search for files and folders by name or title with higher priority for folders by title"); + String boostedQuery4 = "TYPE:('cm:content'^0.5 OR 'cm:folder'^4)^6 AND (cm:name:" + SEARCH_TERM + "^0.5 OR cm:title:" + SEARCH_TERM + "^1.5)"; + searchRequest = req("afts", boostedQuery4); + searchQueryService.expectResultsInOrder(searchRequest, testUser, folderWithTermInTitle.getName(), folderWithTermInName.getName(), fileWithTermInTitle.getName(), fileWithTermInName.getName()); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_phraseBoost() + { + STEP("Search for files by name or TEXT with higher priority for name filter"); + String boostedQuery = "TYPE:'cm:content' AND (cm:name:" + SEARCH_TERM + "^2 OR TEXT:'" + SEARCH_TERM + " searched'^0.1)"; + SearchRequest searchRequest = req("afts", boostedQuery); + searchQueryService.expectResultsInOrder(searchRequest, testUser, fileWithTermInName.getName(), fileWithPhraseInContent.getName()); + + STEP("Search for files by name or TEXT with higher priority for TEXT filter"); + String invertedBoost = "TYPE:'cm:content' AND (cm:name:" + SEARCH_TERM + "^0.1 OR TEXT:'" + SEARCH_TERM + " searched'^2)"; + searchRequest = req("afts", invertedBoost); + searchQueryService.expectResultsInOrder(searchRequest, testUser, fileWithPhraseInContent.getName(), fileWithTermInName.getName()); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_exactTermBoost() + { + STEP("Search for files by exact name or content with higher priority for exact name filter"); + String boostedQuery = "TYPE:'cm:content' AND (=cm:name:" + SEARCH_TERM + ".txt^2 OR =cm:content:" + SEARCH_TERM + "^0.5)"; + SearchRequest searchRequest = req("afts", boostedQuery); + searchQueryService.expectResultsInOrder(searchRequest, testUser, fileWithTermInName.getName(), fileWithPhraseInContent.getName()); + + STEP("Search for files by exact name or content with higher priority for exact content filter"); + String invertedBoost = "TYPE:'cm:content' AND (=cm:name:" + SEARCH_TERM + ".txt^0.1 OR =cm:content:" + SEARCH_TERM + "^3)"; + searchRequest = req("afts", invertedBoost); + searchQueryService.expectResultsInOrder(searchRequest, testUser, fileWithPhraseInContent.getName(), fileWithTermInName.getName()); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_expandedTermBoost() + { + STEP("Search for files by expanded name and two different terms with higher priority for first term"); + String boostedQuery = "TYPE:'cm:content' AND (~cm:name:" + SEARCH_TERM + "^3 OR ~cm:name:" + DIFFERENT_SEARCH_TERM + "^0.1)"; + SearchRequest searchRequest = req("afts", boostedQuery); + searchQueryService.expectResultsInOrder(searchRequest, testUser, fileWithTermInName.getName(), fileWithDifferentTermInName.getName()); + + STEP("Search for files by expanded name and two different terms with higher priority for second term"); + String invertedBoost = "TYPE:'cm:content' AND (~cm:name:" + SEARCH_TERM + "^0.1 OR ~cm:name:" + DIFFERENT_SEARCH_TERM + "^3)"; + searchRequest = req("afts", invertedBoost); + searchQueryService.expectResultsInOrder(searchRequest, testUser, fileWithDifferentTermInName.getName(), fileWithTermInName.getName()); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_fuzzyMatchingBoost() + { + STEP("Fuzzy matching search for files by name or title with higher priority for fuzzy name filter"); + String boostedQuery = "TYPE:'cm:content' AND (cm:name:" + SEARCH_TERM + "~0.7^3 OR cm:title:" + SEARCH_TERM + "^0.01)"; + SearchRequest searchRequest = req("afts", boostedQuery); + searchQueryService.expectResultsInOrder(searchRequest, testUser, fileWithTermInName.getName(), fileWithDifferentTermInName.getName(), fileWithTermInTitle.getName()); + + STEP("Fuzzy matching search for files by name or title with higher priority for fuzzy title filter"); + String invertedBoost = "TYPE:'cm:content' AND (cm:name:" + SEARCH_TERM + "~0.7^0.01 OR cm:title:" + SEARCH_TERM + "^3)"; + searchRequest = req("afts", invertedBoost); + searchQueryService.expectResultsInOrder(searchRequest, testUser, fileWithTermInTitle.getName(), fileWithTermInName.getName(), fileWithDifferentTermInName.getName()); + } + + /** + * Verify if boosts works fine with words proximity search. Files containing terms within specific distance from another one should be returned. + */ + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_proximitySearchBoost() + { + STEP("Search for files by name or proximity TEXT with higher priority for name filter"); + String boostedQuery = "TYPE:'cm:content' AND (cm:name:" + SEARCH_TERM + "^5 OR TEXT:(" + SEARCH_TERM + " *(1) phrase)^0.1)"; + SearchRequest searchRequest = req("afts", boostedQuery); + searchQueryService.expectResultsInOrder(searchRequest, testUser, fileWithTermInName.getName(), fileWithPhraseInContent.getName()); + + STEP("Search for files by name or proximity TEXT with higher priority for proximity TEXT filter"); + String invertedBoost = "TYPE:'cm:content' AND (cm:name:" + SEARCH_TERM + "^0.1 OR TEXT:(" + SEARCH_TERM + " *(1) phrase)^5)"; + searchRequest = req("afts", invertedBoost); + searchQueryService.expectResultsInOrder(searchRequest, testUser, fileWithPhraseInContent.getName(), fileWithTermInName.getName()); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_dateRangeSearchBoost() + { + String timeFrom = creationTime.format(DateTimeFormatter.ISO_DATE_TIME); + String timeTo = afterCreationTime.format(DateTimeFormatter.ISO_DATE_TIME); + + STEP("Search for files and folders by name or creation time range with higher priority for name filter"); + String boostedQuery = "TYPE:('cm:content'^16 OR 'cm:folder'^1) AND (cm:name:" + SEARCH_TERM + "^4 OR cm:created:['" + timeFrom + "' TO '" + timeTo + "']^0.1)^3"; + SearchRequest searchRequest = req("afts", boostedQuery); + searchQueryService.expectResultsInOrder(searchRequest, testUser, fileWithTermInName.getName(), folderWithTermInName.getName(), fileWithTermInTitle.getName(), folderWithTermInTitle.getName()); + + STEP("Search for files and folders by name or creation time range with higher priority for creation time range filter"); + String invertedBoost = "TYPE:('cm:content' OR 'cm:folder'^3) AND (cm:name:" + SEARCH_TERM + "^0.1 OR cm:created:['" + timeFrom + "' TO '" + timeTo + "']^4)^3"; + searchRequest = req("afts", invertedBoost); + searchQueryService.expectResultsInOrder(searchRequest, testUser, folderWithTermInTitle.getName(), fileWithTermInTitle.getName(), folderWithTermInName.getName(), fileWithTermInName.getName()); + } + + /** + * Verify if boosts works fine with words range search. Files containing words from alphabetical range (from 'mountain' to 'phrase', this includes word 'other') should be returned. + */ + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_wordsRangeSearchBoost() + { + String contentPath = "AND PATH:\"/app:company_home/cm:" + testFolder.getName() + "//*\""; + + STEP("Search for files by name or words in content from given range with higher priority for name filter"); + String boostedQuery = "TYPE:'cm:content' AND (cm:name:" + SEARCH_TERM + "^3 OR cm:content:" + SEARCH_TERM + "..phrase^0.1) " + contentPath; + SearchRequest searchRequest = req("afts", boostedQuery); + searchQueryService.expectResultsStartingWithOneOf(searchRequest, testUser, fileWithTermInName.getName()); + searchQueryService.expectResultsFromQuery(searchRequest, testUser, fileWithTermInName.getName(), fileWithPhraseInContent.getName(), fileWithDifferentTermInName.getName()); + + STEP("Search for files by name or words in content from given range with higher priority for words in content from given range filter"); + String invertedBoost = "TYPE:'cm:content' AND (cm:name:" + SEARCH_TERM + "^0.1 OR cm:content:" + SEARCH_TERM + "..phrase^3) " + contentPath; + searchRequest = req("afts", invertedBoost); + searchQueryService.expectResultsStartingWithOneOf(searchRequest, testUser, fileWithPhraseInContent.getName(), fileWithDifferentTermInName.getName()); + searchQueryService.expectResultsFromQuery(searchRequest, testUser, fileWithTermInName.getName(), fileWithPhraseInContent.getName(), fileWithDifferentTermInName.getName()); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_wildcardSearchBoost() + { + STEP("Search for files by wildcard name or title with higher priority for wildcard name filter"); + String wildcardTerm = SEARCH_TERM.replaceFirst("^.", "?"); + String boostedQuery = "TYPE:'cm:content' AND (cm:name:" + wildcardTerm + "^3 OR cm:title:" + SEARCH_TERM + "^0.1)"; + SearchRequest searchRequest = req("afts", boostedQuery); + searchQueryService.expectResultsStartingWithOneOf(searchRequest, testUser, fileWithTermInName.getName(), fileWithDifferentTermInName.getName()); + searchQueryService.expectResultsFromQuery(searchRequest, testUser, fileWithTermInName.getName(), fileWithDifferentTermInName.getName(), fileWithTermInTitle.getName()); + + STEP("Search for files by wildcard name or title with higher priority for wildcard title filter"); + wildcardTerm = SEARCH_TERM.replaceFirst("^.", "*"); + String invertedBoost = "TYPE:'cm:content' AND (cm:name:" + wildcardTerm + "^0.1 OR cm:title:" + SEARCH_TERM + "^3)"; + searchRequest = req("afts", invertedBoost); + searchQueryService.expectResultsStartingWithOneOf(searchRequest, testUser, fileWithTermInTitle.getName()); + searchQueryService.expectResultsFromQuery(searchRequest, testUser, fileWithTermInName.getName(), fileWithDifferentTermInName.getName(), fileWithTermInTitle.getName()); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_invalidNegativeBoost() + { + STEP("Try to search for files by name using negative boost and expect 500 error response"); + String boostedQuery = "TYPE:'cm:content'^-2 AND cm:name:" + SEARCH_TERM; + SearchRequest searchRequest = req("afts", boostedQuery); + searchQueryService.expectErrorFromQuery(searchRequest, testUser, HttpStatus.INTERNAL_SERVER_ERROR, EMPTY); + } + + private ContentModel createRandomFileWithTitle(ContentModel parent, String title) + { + return createRandomFile(parent, title, null, null); + } + + private ContentModel createRandomFileWithTitle(String title) + { + return createRandomFile(title, null, null); + } + + private ContentModel createRandomFile(ContentModel parent, String title, String description, String tag) + { + return createFile(parent, getRandomFile(FileType.TEXT_PLAIN), "dummy content", title, description, tag); + } + + private ContentModel createRandomFile(String title, String description, String tag) + { + return createFile(getRandomFile(FileType.TEXT_PLAIN), "dummy content", title, description, tag); + } + + private ContentModel createFile(ContentModel parent, String filename, String content) + { + return createFile(parent, filename, content, null, null, null); + } + + private ContentModel createFile(String filename, String content) + { + return createFile(filename, content, null, null, null); + } + + private ContentModel createFile(ContentModel parent, String filename, String content, String title, String description, String tag) + { + FileModel fileModel = new FileModel(filename, FileType.TEXT_PLAIN, content); + fileModel.setTitle(title); + fileModel.setDescription(description); + + FileModel file = dataContent + .usingAdmin() + .usingResource(parent) + .createContent(fileModel); + + if (StringUtils.isNotBlank(tag)) + { + dataContent + .usingAdmin() + .usingResource(file) + .addTagToContent(RestTagModel.builder().tag(tag).create()); + } + + return file; + } + + private ContentModel createFile(String filename, String content, String title, String description, String tag) + { + ContentModel contentRoot = new ContentModel("-root-"); + contentRoot.setNodeRef(contentRoot.getName()); + return createFile(contentRoot, filename, content, title, description, tag); + } + + private ContentModel createRandomFolderWithTitle(ContentModel parent, String title) + { + return createFolder(parent, getRandomName("folder"), title, null, null); + } + + private ContentModel createFolder(ContentModel parent, String folderName) + { + return createFolder(parent, folderName, null, null, null); + } + + private ContentModel createFolder(String folderName) + { + return createFolder(folderName, null, null, null); + } + + private ContentModel createFolder(ContentModel parent, String folderName, String title, String description, String tag) + { + FolderModel folderModel = new FolderModel(folderName, title, description); + + FolderModel folder = dataContent + .usingAdmin() + .usingResource(parent) + .createFolder(folderModel); + + if (StringUtils.isNotBlank(tag)) + { + dataContent + .usingAdmin() + .usingResource(folder) + .addTagToContent(RestTagModel.builder().tag(tag).create()); + } + + return folder; + } + + private ContentModel createFolder(String folderName, String title, String description, String tag) + { + ContentModel contentRoot = new ContentModel("-root-"); + contentRoot.setNodeRef(contentRoot.getName()); + return createFolder(contentRoot, folderName, title, description, tag); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchCMISPathTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchCMISPathTests.java new file mode 100644 index 000000000..0b44a4d6d --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchCMISPathTests.java @@ -0,0 +1,145 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.alfresco.elasticsearch.SearchQueryService.req; +import static org.alfresco.tas.TestDataUtility.getAlphabeticUUID; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; + +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +/** + * Tests for CMIS queries that require path indexing against Elasticsearch. + */ +public class ElasticsearchCMISPathTests extends AbstractTestNGSpringContextTests +{ + private static final String PREFIX = getAlphabeticUUID(); + private static final String FOLDER_0_NAME = PREFIX + "_folder0"; + private static final String FOLDER_00_NAME = PREFIX + "_folder00"; + private static final String FOLDER_000_NAME = PREFIX + "_folder000"; + private static final String FOLDER_1_NAME = PREFIX + "_folder1"; + private static final String DOC_0000_NAME = PREFIX + "_doc0000.txt"; + private static final String DOC_00_NAME = PREFIX + "_doc00.txt"; + private static final String DOC_01_NAME = PREFIX + "_doc01.txt"; + private static final String DOC_10_NAME = PREFIX + "_doc10.txt"; + + @Autowired + private DataUser dataUser; + @Autowired + private DataContent dataContent; + @Autowired + private DataSite dataSite; + @Autowired + private ServerHealth serverHealth; + @Autowired + protected SearchQueryService searchQueryService; + + private UserModel user; + private SiteModel siteModel; + private FolderModel folder0; + private FolderModel folder00; + private FolderModel folder000; + private FolderModel folder1; + private FileModel document0000; + private FileModel document00; + private FileModel document01; + private FileModel document10; + + /** + * Data will be prepared using the schema below: + * + *
+     * Site
+     * + Document Library
+     *   +-folder0
+     *   | +-folder00
+     *   | | +-folder000
+     *   | |   +-document0000
+     *   | +-document00
+     *   | +-document01
+     *   +-folder1
+     *   +-document10
+     * 
+ */ + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + serverHealth.assertServerIsOnline(); + + user = dataUser.createRandomTestUser(); + + siteModel = dataSite.usingUser(user).createPrivateRandomSite(); + + folder0 = dataContent.usingUser(user).usingSite(siteModel).createFolderCmisApi(FOLDER_0_NAME); + folder1 = dataContent.usingUser(user).usingSite(siteModel).createFolderCmisApi(FOLDER_1_NAME); + + folder00 = dataContent.usingUser(user).usingResource(folder0).createFolderCmisApi(FOLDER_00_NAME); + folder000 = dataContent.usingUser(user).usingResource(folder00).createFolderCmisApi(FOLDER_000_NAME); + + document0000 = createContent(DOC_0000_NAME, "This is document 0000", folder000, user); + document00 = createContent(DOC_00_NAME, "This is document 00", folder0, user); + document01 = createContent(DOC_01_NAME, "This is document 01", folder0, user); + document10 = createContent(DOC_10_NAME, "This is document 10", folder1, user); + } + + @Test(groups = TestGroup.SEARCH) + public void inTreeQuery_selectSubfolders() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:folder WHERE IN_TREE('" + folder0.getNodeRef() + "')"); + searchQueryService.expectResultsFromQuery(query, user, FOLDER_00_NAME, FOLDER_000_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void inTreeQuery_selectDocuments() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE IN_TREE('" + folder0.getNodeRef() + "')"); + searchQueryService.expectResultsFromQuery(query, user, DOC_0000_NAME, DOC_00_NAME, DOC_01_NAME); + } + + private FileModel createContent(String filename, String content, FolderModel folderModel, UserModel user) + { + FileModel fileModel = new FileModel(filename, FileType.TEXT_PLAIN, content); + return dataContent.usingUser(user).usingResource(folderModel).createContent(fileModel); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchCMISTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchCMISTests.java new file mode 100644 index 000000000..f945a7e08 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchCMISTests.java @@ -0,0 +1,604 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static java.util.stream.Collectors.toList; + +import static org.alfresco.elasticsearch.SearchQueryService.req; +import static org.alfresco.tas.TestDataUtility.getAlphabeticUUID; +import static org.alfresco.utility.report.log.Step.STEP; + +import java.io.IOException; +import java.time.Instant; +import java.util.GregorianCalendar; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.Utility; +import org.alfresco.utility.constants.UserRole; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; + +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +/** + * In this test we are verifying end-to-end the indexing and CMIS queries against Elasticsearch. In order to test ACLs we created 2 sites and 3 users. + */ +public class ElasticsearchCMISTests extends AbstractTestNGSpringContextTests +{ + private static final String PREFIX = getAlphabeticUUID(); + private static final String SUFFIX = getAlphabeticUUID(); + private static final String UNIQUE_WORD = getAlphabeticUUID(); + private static final String FILE_0_NAME = PREFIX + "_test.txt" + SUFFIX; + private static final String FILE_1_NAME = "internal_" + PREFIX + "_and_" + SUFFIX + ".txt"; + private static final String FILE_2_NAME = PREFIX + "_user2doc_" + SUFFIX; + /** This is a file that user 1 doesn't have access to and so shouldn't be returned in their search results. */ + private static final String USER_2_FILE_NAME = PREFIX + "_user2only_" + SUFFIX; + private static final String FOLDER_PREFIX = getAlphabeticUUID(); + private static final String FOLDER_0_NAME = FOLDER_PREFIX + "_folder0"; + private static final String FOLDER_1_NAME = FOLDER_PREFIX + "_folder1"; + private static final int X_DIMENSION = 48; + + @Autowired + private DataUser dataUser; + + @Autowired + private DataContent dataContent; + + @Autowired + private DataSite dataSite; + + @Autowired + private ServerHealth serverHealth; + + @Autowired + protected SearchQueryService searchQueryService; + + private UserModel user1; + private UserModel user2; + private UserModel userMultiSite; + private SiteModel siteModel1; + private SiteModel siteModel2; + private FileModel file0; + private List fileCreationDates; + private String file3Name; + /** + * Scoping predicate restricting the result universe to the 4 documents user1 owns in this class. Used to AND into negative / open-range queries so they aren't polluted by bootstrap content or by files created in other test classes that share the same Spring context. + */ + private String user1FilesScope; + /** + * Scoping predicate restricting to file3 (the only EXIF-bearing image we upload). Used to AND into negative / open-range integer queries on exif:pixelXDimension. + */ + private String file3Scope; + + /** + * Data will be prepared using the schema below: + *

+ * Site1: - Users: user1, userMultiSite - Documents: FILE_0_NAME (owner: user1), FILE_1_NAME (owner: user1), FILE_2_NAME (owner: user2), file3Name (owner: user1) - Folders: FOLDER_0_NAME (owner: user1), FOLDER_1_NAME (owner: user1) + *

+ * Site2: - Users: user2, userMultiSite - Documents: USER_2_FILE_NAME (owner: user2) + */ + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws IOException, InterruptedException + { + serverHealth.assertServerIsOnline(); + + user1 = dataUser.createRandomTestUser(); + user2 = dataUser.createRandomTestUser(); + userMultiSite = dataUser.createRandomTestUser(); + + siteModel1 = dataSite.usingUser(user1).createPrivateRandomSite(); + siteModel2 = dataSite.usingUser(user2).createPrivateRandomSite(); + + dataUser.addUserToSite(user2, siteModel1, UserRole.SiteContributor); + dataUser.addUserToSite(userMultiSite, siteModel1, UserRole.SiteContributor); + dataUser.addUserToSite(userMultiSite, siteModel2, UserRole.SiteContributor); + + file0 = createContent(FILE_0_NAME, "This is the first test containing " + UNIQUE_WORD, siteModel1, user1); + FileModel file1 = createContent(FILE_1_NAME, "This is another TEST file containing " + UNIQUE_WORD, siteModel1, user1); + FileModel file2 = createContent(FILE_2_NAME, "This Test file is owned by user2 " + UNIQUE_WORD, siteModel1, user2); + FileModel file3 = uploadDocument("test-data/alfresco-logo.png", user1, siteModel1); + file3Name = file3.getName(); + + fileCreationDates = getCreationDates(file0, file1, file2); + + // Remove user 2 from site, but he keeps ownership on FILE_2_NAME. + dataUser.removeUserFromSite(user2, siteModel1); + // Also create another file that only user 2 has access to. + createContent(USER_2_FILE_NAME, "This is a test file that user1 does not have access to, but it still contains " + UNIQUE_WORD, siteModel2, user2); + + dataContent.usingUser(user1).usingSite(siteModel1).createFolder(new FolderModel(FOLDER_0_NAME)); + dataContent.usingUser(user1).usingSite(siteModel1).createFolder(new FolderModel(FOLDER_1_NAME)); + + // Pre-build scoping predicates used by the negative / open-range tests below. + user1FilesScope = "cmis:name IN ('" + FILE_0_NAME + "', '" + FILE_1_NAME + "', '" + FILE_2_NAME + "', '" + file3Name + "')"; + file3Scope = "D.cmis:name = '" + file3Name + "'"; + + STEP("Wait for batch indexer to index the last-created node (FOLDER_1_NAME)"); + SearchRequest probe = req("cmis", "SELECT * FROM cmis:folder WHERE cmis:name = '" + FOLDER_1_NAME + "'"); + Utility.sleep(500, 60000, () -> searchQueryService.expectResultsFromQuery(probe, user1, FOLDER_1_NAME)); + } + + @Test(groups = TestGroup.SEARCH) + public void basicQuery() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:document"); + searchQueryService.expectResultsInclude(query, user1, FILE_0_NAME, FILE_1_NAME, FILE_2_NAME, file3Name); + } + + @Test(groups = TestGroup.SEARCH) + public void objectIdQuery() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE cmis:objectId = '" + file0.getNodeRef() + "'"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_0_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void objectTypeIdQuery() + { + SearchRequest query1 = req("cmis", "SELECT * FROM cmis:folder WHERE cmis:objectTypeId = 'cmis:folder'"); + searchQueryService.expectResultsInclude(query1, user1, "documentLibrary", FOLDER_0_NAME, FOLDER_1_NAME, user1.getUsername()); + + SearchRequest query2 = req("cmis", "SELECT * FROM cmis:folder WHERE cmis:objectTypeId = 'F:st:site'"); + searchQueryService.expectResultsInclude(query2, user1, siteModel1.getId()); + } + + @Test(groups = TestGroup.SEARCH) + public void baseTypeIdQuery() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:folder WHERE cmis:baseTypeId = 'cmis:folder'"); + searchQueryService.expectResultsInclude(query, user1, "documentLibrary", FOLDER_0_NAME, FOLDER_1_NAME, user1.getUsername(), siteModel1.getId()); + } + + @Test(groups = TestGroup.SEARCH) + public void isNullQuery() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:folder WHERE cmis:description IS NULL"); + searchQueryService.expectResultsInclude(query, user1, "documentLibrary", FOLDER_0_NAME, FOLDER_1_NAME, user1.getUsername()); + } + + @Test(groups = TestGroup.SEARCH) + public void isNotNullQuery() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:folder WHERE cmis:description IS NOT NULL"); + searchQueryService.expectResultsInclude(query, user1, siteModel1.getId()); + } + + @Test(groups = TestGroup.SEARCH) + public void matchNamesLikePrefix() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE cmis:name LIKE '" + PREFIX + "%'"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_0_NAME, FILE_1_NAME, FILE_2_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void matchNamesLikeSuffix() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE cmis:name LIKE '%" + SUFFIX + "'"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_0_NAME, FILE_1_NAME, FILE_2_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void matchContentOfFile() + { + // Check the query is case insensitive. + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE CONTAINS('" + UNIQUE_WORD + "')"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_0_NAME, FILE_1_NAME, FILE_2_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void checkPermissionForUser2() + { + // Reuse the prefix query to check which documents user2 can access. + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE cmis:name LIKE '" + PREFIX + "%'"); + searchQueryService.expectResultsFromQuery(query, user2, FILE_2_NAME, USER_2_FILE_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void matchDocumentName() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE cmis:name = '" + FILE_0_NAME + "'"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_0_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void doesNotMatchDocumentName() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE " + user1FilesScope + " AND cmis:name <> '" + FILE_0_NAME + "'"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_1_NAME, FILE_2_NAME, file3Name); + } + + @Test(groups = TestGroup.SEARCH) + public void checkOrderByAscSyntax() + { + SearchRequest query = req("cmis", "SELECT cmis:name FROM cmis:document WHERE cmis:name IN('" + FILE_0_NAME + "','" + FILE_1_NAME + "','" + FILE_2_NAME + "') ORDER BY cmis:name ASC"); + searchQueryService.expectResultsInOrder(query, user1, true, FILE_0_NAME, FILE_1_NAME, FILE_2_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void checkOrderByDescSyntax() + { + SearchRequest query = req("cmis", "SELECT cmis:name FROM cmis:document WHERE cmis:name IN('" + FILE_0_NAME + "','" + FILE_1_NAME + "','" + FILE_2_NAME + "') ORDER BY cmis:name DESC"); + searchQueryService.expectResultsInOrder(query, user1, false, FILE_0_NAME, FILE_1_NAME, FILE_2_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void checkInSyntax() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE cmis:name IN ('" + FILE_0_NAME + "', '" + FILE_1_NAME + "')"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_0_NAME, FILE_1_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void checkNotInSyntax() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE " + user1FilesScope + " AND cmis:name NOT IN ('" + FILE_0_NAME + "', '" + FILE_1_NAME + "')"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_2_NAME, file3Name); + } + + @Test(groups = TestGroup.SEARCH) + public void checkAfterDateSyntax() + { + String file0CreationDate = fileCreationDates.get(0); + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE cmis:creationDate > TIMESTAMP '" + file0CreationDate + "'"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_1_NAME, FILE_2_NAME, file3Name); + } + + @Test(groups = TestGroup.SEARCH) + public void checkAfterOrSameDateSyntax() + { + String file0CreationDate = fileCreationDates.get(0); + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE cmis:creationDate >= TIMESTAMP '" + file0CreationDate + "'"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_0_NAME, FILE_1_NAME, FILE_2_NAME, file3Name); + } + + @Test(groups = TestGroup.SEARCH) + public void checkBeforeDateSyntax() + { + String file2CreationDate = fileCreationDates.get(2); + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE " + user1FilesScope + " AND cmis:creationDate < TIMESTAMP '" + file2CreationDate + "'"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_0_NAME, FILE_1_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void checkBeforeOrSameDateSyntax() + { + String file2CreationDate = fileCreationDates.get(2); + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE " + user1FilesScope + " AND cmis:creationDate <= TIMESTAMP '" + file2CreationDate + "'"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_0_NAME, FILE_1_NAME, FILE_2_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void checkMatchesDateSyntax() + { + String file0CreationDate = fileCreationDates.get(0); + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE cmis:creationDate = TIMESTAMP '" + file0CreationDate + "'"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_0_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void checkDoesNotMatchDateSyntax() + { + String file0CreationDate = fileCreationDates.get(0); + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE " + user1FilesScope + " AND cmis:creationDate <> TIMESTAMP '" + file0CreationDate + "'"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_1_NAME, FILE_2_NAME, file3Name); + } + + @Test(groups = TestGroup.SEARCH) + public void checkInDatesSyntax() + { + String file0CreationDate = fileCreationDates.get(0); + String file1CreationDate = fileCreationDates.get(1); + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE cmis:creationDate IN (TIMESTAMP '" + file0CreationDate + "', TIMESTAMP '" + file1CreationDate + "')"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_0_NAME, FILE_1_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void checkNotInDatesSyntax() + { + String file0CreationDate = fileCreationDates.get(0); + String file1CreationDate = fileCreationDates.get(1); + SearchRequest query = req("cmis", "SELECT * FROM cmis:document WHERE " + user1FilesScope + " AND cmis:creationDate NOT IN (TIMESTAMP '" + file0CreationDate + "', TIMESTAMP '" + file1CreationDate + "')"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_2_NAME, file3Name); + } + + @Test(groups = TestGroup.SEARCH) + public void checkEqualIntegerSyntax() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:document AS D JOIN exif:exif AS E ON D.cmis:objectId = E.cmis:objectId WHERE E.exif:pixelXDimension = " + X_DIMENSION); + query.setInclude(List.of("properties")); + searchQueryService.expectResultsFromQuery(query, user1, file3Name); + } + + @Test(groups = TestGroup.SEARCH) + public void checkDifferentThanIntegerSyntax() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:document AS D JOIN exif:exif AS E ON D.cmis:objectId = E.cmis:objectId WHERE " + file3Scope + " AND E.exif:pixelXDimension <> " + X_DIMENSION); + query.setInclude(List.of("properties")); + searchQueryService.expectNoResultsFromQuery(query, user1); + } + + @Test(groups = TestGroup.SEARCH) + public void checkGreaterThanIntegerSyntax() + { + int lessThanX = X_DIMENSION - 1; + SearchRequest query = req("cmis", "SELECT * FROM cmis:document AS D JOIN exif:exif AS E ON D.cmis:objectId = E.cmis:objectId WHERE " + file3Scope + " AND E.exif:pixelXDimension > " + lessThanX); + query.setInclude(List.of("properties")); + searchQueryService.expectResultsFromQuery(query, user1, file3Name); + + query = req("cmis", "SELECT * FROM cmis:document AS D JOIN exif:exif AS E ON D.cmis:objectId = E.cmis:objectId WHERE " + file3Scope + " AND E.exif:pixelXDimension > " + X_DIMENSION); + query.setInclude(List.of("properties")); + searchQueryService.expectNoResultsFromQuery(query, user1); + } + + @Test(groups = TestGroup.SEARCH) + public void checkGreaterThanOrEqualIntegerSyntax() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:document AS D JOIN exif:exif AS E ON D.cmis:objectId = E.cmis:objectId WHERE " + file3Scope + " AND E.exif:pixelXDimension >= " + X_DIMENSION); + query.setInclude(List.of("properties")); + searchQueryService.expectResultsFromQuery(query, user1, file3Name); + + int moreThanX = X_DIMENSION + 1; + query = req("cmis", "SELECT * FROM cmis:document AS D JOIN exif:exif AS E ON D.cmis:objectId = E.cmis:objectId WHERE " + file3Scope + " AND E.exif:pixelXDimension >= " + moreThanX); + query.setInclude(List.of("properties")); + searchQueryService.expectNoResultsFromQuery(query, user1); + } + + @Test(groups = TestGroup.SEARCH) + public void checkLessThanIntegerSyntax() + { + int moreThanX = X_DIMENSION + 1; + SearchRequest query = req("cmis", "SELECT * FROM cmis:document AS D JOIN exif:exif AS E ON D.cmis:objectId = E.cmis:objectId WHERE E.exif:pixelXDimension < " + moreThanX); + query.setInclude(List.of("properties")); + searchQueryService.expectResultsFromQuery(query, user1, file3Name); + + query = req("cmis", "SELECT * FROM cmis:document AS D JOIN exif:exif AS E ON D.cmis:objectId = E.cmis:objectId WHERE E.exif:pixelXDimension < " + X_DIMENSION); + query.setInclude(List.of("properties")); + searchQueryService.expectNoResultsFromQuery(query, user1); + } + + @Test(groups = TestGroup.SEARCH) + public void checkLessThanOrEqualIntegerSyntax() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:document AS D JOIN exif:exif AS E ON D.cmis:objectId = E.cmis:objectId WHERE E.exif:pixelXDimension <= " + X_DIMENSION); + query.setInclude(List.of("properties")); + searchQueryService.expectResultsFromQuery(query, user1, file3Name); + + int lessThanX = X_DIMENSION - 1; + query = req("cmis", "SELECT * FROM cmis:document AS D JOIN exif:exif AS E ON D.cmis:objectId = E.cmis:objectId WHERE E.exif:pixelXDimension <= " + lessThanX); + query.setInclude(List.of("properties")); + searchQueryService.expectNoResultsFromQuery(query, user1); + } + + @Test(groups = TestGroup.SEARCH) + public void checkInIntegersSyntax() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:document AS D JOIN exif:exif AS E ON D.cmis:objectId = E.cmis:objectId WHERE E.exif:pixelXDimension IN (" + X_DIMENSION + ", " + 0 + ")"); + query.setInclude(List.of("properties")); + searchQueryService.expectResultsFromQuery(query, user1, file3Name); + + query = req("cmis", "SELECT * FROM cmis:document AS D JOIN exif:exif AS E ON D.cmis:objectId = E.cmis:objectId WHERE E.exif:pixelXDimension IN (" + 0 + ", " + -1 + ")"); + query.setInclude(List.of("properties")); + searchQueryService.expectNoResultsFromQuery(query, user1); + } + + @Test(groups = TestGroup.SEARCH) + public void checkNotInIntegersSyntax() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:document AS D JOIN exif:exif AS E ON D.cmis:objectId = E.cmis:objectId WHERE " + file3Scope + " AND E.exif:pixelXDimension NOT IN (" + X_DIMENSION + ", " + 0 + ")"); + query.setInclude(List.of("properties")); + searchQueryService.expectNoResultsFromQuery(query, user1); + + query = req("cmis", "SELECT * FROM cmis:document AS D JOIN exif:exif AS E ON D.cmis:objectId = E.cmis:objectId WHERE " + file3Scope + " AND E.exif:pixelXDimension NOT IN (" + 0 + ", " + -1 + ")"); + query.setInclude(List.of("properties")); + searchQueryService.expectResultsFromQuery(query, user1, file3Name); + } + + @Test(groups = TestGroup.SEARCH) + public void negative_basicCMISQuery_missingFrom() + { + // note: ideally 400 but currently 500 (also for Solr) :-( + + SearchRequest query1 = req("cmis", "SELECT *"); + searchQueryService.expectErrorFromQuery(query1, user1, HttpStatus.INTERNAL_SERVER_ERROR, "expecting FROM"); + + SearchRequest query2 = req("cmis", "SELECT * FROM"); + searchQueryService.expectErrorFromQuery(query2, user1, HttpStatus.INTERNAL_SERVER_ERROR, "no viable alternative at input"); + } + + @Test(groups = TestGroup.SEARCH) + public void negative_basicCMISQuery_invalidType() + { + // note: ideally 400 but currently 500 (also for Solr) :-( + SearchRequest query = req("SELECT * FROM cmis:unknown"); + searchQueryService.expectErrorFromQuery(query, user1, HttpStatus.INTERNAL_SERVER_ERROR, "Unknown property: {http://www.alfresco.org/model/content/1.0}cmis"); + } + + @Test(groups = TestGroup.SEARCH) + public void negative_objectTypeIdQuery_invalidType() + { + // note: ideally 400 but currently 500 (also for Solr) :-( + SearchRequest query = req("SELECT * FROM cmis:folder WHERE cmis:objectTypeId = 'unknown:site'"); + searchQueryService.expectErrorFromQuery(query, user1, HttpStatus.INTERNAL_SERVER_ERROR, "Unknown property: {http://www.alfresco.org/model/content/1.0}cmis"); + } + + @Test(groups = TestGroup.SEARCH) + public void negative_isNullQuery_invalidField() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:folder WHERE cmis:unknown IS NULL"); + searchQueryService.expectErrorFromQuery(query, user1, HttpStatus.INTERNAL_SERVER_ERROR, "Unknown column/property cmis:unknown"); + } + + @Test(groups = TestGroup.SEARCH) + public void negative_isNotNullQuery_invalidField() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:folder WHERE cmis:unknown IS NOT NULL"); + searchQueryService.expectErrorFromQuery(query, user1, HttpStatus.INTERNAL_SERVER_ERROR, "Unknown column/property cmis:unknown"); + } + + @Test(groups = TestGroup.SEARCH) + public void negative_basicCMISQuery_invalidFieldName() + { + // note: ideally 400 but currently 500 (also for Solr) :-( + + SearchRequest query1 = req("SELECT cmis:unknown FROM cmis:document"); + searchQueryService.expectErrorFromQuery(query1, user1, HttpStatus.INTERNAL_SERVER_ERROR, "Unknown property: {http://www.alfresco.org/model/content/1.0}cmis"); + + SearchRequest query2 = req("SELECT cm:unknown FROM cmis:document"); + searchQueryService.expectErrorFromQuery(query2, user1, HttpStatus.INTERNAL_SERVER_ERROR, "Unknown property: {http://www.alfresco.org/model/content/1.0}cm"); + + SearchRequest query3 = req("SELECT my:custom FROM cmis:document"); + searchQueryService.expectErrorFromQuery(query3, user1, HttpStatus.INTERNAL_SERVER_ERROR, "Unknown property: {http://www.alfresco.org/model/content/1.0}my"); + } + + @Test(groups = TestGroup.SEARCH) + public void negative_baseTypeIdQuery_invalidType() + { + // note: ideally 400 but currently 500 (also for Solr) :-( + SearchRequest query = req("SELECT * FROM cmis:folder WHERE cmis:baseTypeId = 'cmis:unknown'"); + searchQueryService.expectErrorFromQuery(query, user1, HttpStatus.INTERNAL_SERVER_ERROR, "Unknown property: {http://www.alfresco.org/model/content/1.0}cmis"); + } + + @Test(groups = TestGroup.SEARCH) + public void selectFolders() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:folder"); + searchQueryService.expectResultsInclude(query, user1, + // We expect the site and the document library, ... + siteModel1.getId(), "documentLibrary", + // the two folders, ... + FOLDER_0_NAME, FOLDER_1_NAME, + // and the user home for the user performing the query. + user1.getUsername()); + } + + @Test(groups = TestGroup.SEARCH) + public void selectItems() + { + SearchRequest query = req("cmis", "SELECT * FROM cmis:item"); + // This error is consistent with the way the DB and Solr handle the request. + searchQueryService.expectErrorFromQuery(query, user1, HttpStatus.INTERNAL_SERVER_ERROR, "Type is not queryable cmis:item"); + } + + @Test(groups = TestGroup.SEARCH) + public void selectObjects() + { + SearchRequest query = req("cmis", "SELECT * FROM cm:cmobject WHERE cmis:name IN ('" + FILE_0_NAME + "', '" + FOLDER_0_NAME + "')"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_0_NAME, FOLDER_0_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void selectPeople() + { + SearchRequest query = req("cmis", "SELECT * FROM cm:person"); + searchQueryService.expectNodeTypesFromQuery(query, user1, "cm:person"); + } + + @Test(groups = TestGroup.SEARCH) + public void tableAlias() + { + SearchRequest query = req("cmis", "SELECT d.cmis:name FROM cmis:document d WHERE d.cmis:name IN ('" + FILE_0_NAME + "')"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_0_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void tableAliasWithAs() + { + SearchRequest query = req("cmis", "SELECT d.cmis:name FROM cmis:document AS d WHERE d.cmis:name IN ('" + FILE_0_NAME + "')"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_0_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void tableAlias_mismatchedAliasInSelect() + { + SearchRequest query = req("cmis", "SELECT z.cmis:name FROM cmis:document d WHERE d.cmis:name IN ('" + FILE_0_NAME + "')"); + searchQueryService.expectErrorFromQuery(query, user1, HttpStatus.INTERNAL_SERVER_ERROR, "No selector for z"); + } + + @Test(groups = TestGroup.SEARCH) + public void tableAlias_mismatchedAliasInWhere() + { + SearchRequest query = req("cmis", "SELECT d.cmis:name FROM cmis:document d WHERE z.cmis:name IN ('" + FILE_0_NAME + "')"); + searchQueryService.expectErrorFromQuery(query, user1, HttpStatus.INTERNAL_SERVER_ERROR, "No selector for z"); + } + + @Test(groups = TestGroup.SEARCH) + public void tableJoin() + { + SearchRequest query = req("cmis", "SELECT t.*, d.* FROM cm:titled t JOIN cmis:document d ON t.cmis:objectId = d.cmis:objectId WHERE d.cmis:name = '" + FILE_0_NAME + "'"); + searchQueryService.expectResultsFromQuery(query, user1, FILE_0_NAME); + } + + private FileModel createContent(String filename, String content, SiteModel site, UserModel user) + { + FileModel fileModel = new FileModel(filename, FileType.TEXT_PLAIN, content); + return dataContent.usingUser(user).usingSite(site) + .createContent(fileModel); + } + + private List getCreationDates(FileModel... files) + { + return getCreationDates(user1, siteModel1, files); + } + + private List getCreationDates(UserModel user, SiteModel site, FileModel... files) + { + return List.of(files) + .stream() + .map(FileModel::getCmisLocation) + .map(cmisLocation -> dataContent.usingUser(user).usingSite(site).getCMISDocument(cmisLocation)) + .map(cmisDocument -> cmisDocument. getProperty("cmis:creationDate")) + .map(creationDateProperty -> creationDateProperty. getValue()) + .map(GregorianCalendar::toInstant) + .map(Instant::toString) + .collect(toList()); + } + + private FileModel uploadDocument(String filePath, UserModel user, SiteModel site) throws IOException + { + ClassPathResource toUpload = new ClassPathResource(filePath); + return dataContent.usingUser(user) + .usingSite(site) + .uploadDocument(toUpload.getFile()); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchCategoriesCountTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchCategoriesCountTests.java new file mode 100644 index 000000000..30a6b00b2 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchCategoriesCountTests.java @@ -0,0 +1,311 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.OK; +import static org.testng.Assert.assertTrue; + +import static org.alfresco.utility.data.RandomData.getRandomName; +import static org.alfresco.utility.report.log.Step.STEP; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.dataprep.CMISUtil; +import org.alfresco.rest.core.RestWrapper; +import org.alfresco.rest.model.RestCategoryLinkBodyModel; +import org.alfresco.rest.model.RestCategoryModel; +import org.alfresco.rest.model.RestCategoryModelsCollection; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.Utility; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.RepoTestModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; + +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +public class ElasticsearchCategoriesCountTests extends AbstractTestNGSpringContextTests +{ + protected static final String INCLUDE_COUNT_PARAM = "count"; + protected static final String ROOT_CATEGORY_ID = "-root-"; + protected static final String CATEGORY_NAME_PREFIX = "CategoryName"; + protected static final String FIELD_NAME = "name"; + protected static final String FIELD_ID = "id"; + protected static final String FIELD_COUNT = "count"; + + @Autowired + private ServerHealth serverHealth; + @Autowired + private DataUser dataUser; + @Autowired + private DataSite dataSite; + @Autowired + private DataContent dataContent; + @Autowired + private RestWrapper restClient; + + private UserModel user; + private SiteModel site; + private RestCategoryModel categoryLinkedWithFolder; + private RestCategoryModel categoryLinkedWithFile; + private RestCategoryModel categoryLinkedWithBoth; + private RestCategoryModel notLinkedCategory; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws InterruptedException + { + serverHealth.assertServerIsOnline(); + + STEP("Create user and site"); + user = dataUser.createRandomTestUser(); + site = dataSite.usingUser(user).createPublicRandomSite(); + + STEP("Create a folder, file in it and few categories"); + final FolderModel folder = dataContent.usingUser(user).usingSite(site).createFolder(); + final FileModel file = dataContent.usingUser(user).usingResource(folder).createContent(CMISUtil.DocumentType.TEXT_PLAIN); + categoryLinkedWithFolder = prepareCategoryUnderRoot(); + categoryLinkedWithFile = prepareCategoryUnderRoot(); + categoryLinkedWithBoth = prepareCategoryUnder(prepareCategoryUnderRoot()); + notLinkedCategory = prepareCategoryUnderRoot(); + + STEP("Link folder and file to categories"); + linkContentToCategories(folder, categoryLinkedWithFolder, categoryLinkedWithBoth); + linkContentToCategories(file, categoryLinkedWithFile, categoryLinkedWithBoth); + + STEP("Wait for indexing to complete"); + Utility.sleep(500, 60000, () -> restClient.authenticateUser(user) + .withCoreAPI() + .usingCategory(categoryLinkedWithBoth) + .include(INCLUDE_COUNT_PARAM) + .getCategory() + .assertThat() + .field(FIELD_COUNT) + .is(2)); + } + + @AfterClass + public void dataCleanup() + { + STEP("Remove site and user"); + dataSite.usingUser(user).deleteSite(site); + dataUser.deleteUser(user); + } + + /** + * Verify count for a category linked with file and folder. + */ + @Test(groups = {TestGroup.REST_API}) + public void testGetCategoryById_includeCount() + { + STEP("Get linked category and verify if count is higher than 0"); + final RestCategoryModel actualCategory = restClient.authenticateUser(user) + .withCoreAPI() + .usingCategory(categoryLinkedWithBoth) + .include(INCLUDE_COUNT_PARAM) + .getCategory(); + + restClient.assertStatusCodeIs(OK); + actualCategory.assertThat().field(FIELD_ID).is(categoryLinkedWithBoth.getId()); + actualCategory.assertThat().field(FIELD_COUNT).is(2); + } + + /** + * Verify count for a category not linked with any content. + */ + @Test(groups = {TestGroup.REST_API}) + public void testGetCategoryById_includeCountForNonLinkedCategory() + { + STEP("Get non-linked category and verify if count is 0"); + final RestCategoryModel actualCategory = restClient.authenticateUser(user) + .withCoreAPI() + .usingCategory(notLinkedCategory) + .include(INCLUDE_COUNT_PARAM) + .getCategory(); + + restClient.assertStatusCodeIs(OK); + actualCategory.assertThat().field(FIELD_ID).is(notLinkedCategory.getId()); + actualCategory.assertThat().field(FIELD_COUNT).is(0); + } + + /** + * Verify count for three categories: linked with file, linked with folder and third not linked to any content. + */ + @Test(groups = {TestGroup.REST_API}) + public void testGetCategories_includeCount() + { + STEP("Get few categories and verify its counts"); + final RestCategoryModel parentCategory = createCategoryModelWithId(ROOT_CATEGORY_ID); + final RestCategoryModelsCollection actualCategories = restClient.authenticateUser(user) + .withCoreAPI() + .usingCategory(parentCategory) + .include(INCLUDE_COUNT_PARAM) + .getCategoryChildren(); + + restClient.assertStatusCodeIs(OK); + assertTrue(actualCategories.getEntries().stream() + .map(RestCategoryModel::onModel) + .anyMatch(category -> category.getId().equals(categoryLinkedWithFolder.getId()) && category.getCount() == 1)); + assertTrue(actualCategories.getEntries().stream() + .map(RestCategoryModel::onModel) + .anyMatch(category -> category.getId().equals(categoryLinkedWithFile.getId()) && category.getCount() == 1)); + assertTrue(actualCategories.getEntries().stream() + .map(RestCategoryModel::onModel) + .anyMatch(category -> category.getId().equals(notLinkedCategory.getId()) && category.getCount() == 0)); + } + + /** + * Create category and verify that its count is 0. + */ + @Test(groups = {TestGroup.REST_API}) + public void testCreateCategory_includingCount() + { + STEP("Create a category under root and verify if count is 0"); + final String categoryName = getRandomName("Category"); + final RestCategoryModel rootCategory = createCategoryModelWithId(ROOT_CATEGORY_ID); + final RestCategoryModel aCategory = createCategoryModelWithName(categoryName); + final RestCategoryModel createdCategory = restClient.authenticateUser(dataUser.getAdminUser()) + .withCoreAPI() + .include(INCLUDE_COUNT_PARAM) + .usingCategory(rootCategory) + .createSingleCategory(aCategory); + + STEP("Create a category under root category (as admin)"); + restClient.assertStatusCodeIs(CREATED); + createdCategory.assertThat().field(FIELD_NAME).is(categoryName); + createdCategory.assertThat().field(FIELD_COUNT).is(0); + } + + /** + * Update category linked to file and folder and verify that its count is 2. + */ + @Test(groups = {TestGroup.REST_API}) + public void testUpdateCategory_includeCount() + { + STEP("Update linked category and verify if count is higher than 0"); + final String categoryNewName = getRandomName("NewCategoryName"); + final RestCategoryModel fixedCategoryModel = createCategoryModelWithName(categoryNewName); + final RestCategoryModel updatedCategory = restClient.authenticateUser(dataUser.getAdminUser()) + .withCoreAPI() + .usingCategory(categoryLinkedWithBoth) + .include(INCLUDE_COUNT_PARAM) + .updateCategory(fixedCategoryModel); + + restClient.assertStatusCodeIs(OK); + updatedCategory.assertThat().field(FIELD_ID).is(categoryLinkedWithBoth.getId()); + updatedCategory.assertThat().field(FIELD_COUNT).is(2); + } + + /** + * Update category not linked to any content and verify that its count is 0. + */ + @Test(groups = {TestGroup.REST_API}) + public void testUpdateCategory_includeCountForNonLinkedCategory() + { + STEP("Update non-linked category and verify if count is 0"); + final String categoryNewName = getRandomName("NewCategoryName"); + final RestCategoryModel fixedCategoryModel = createCategoryModelWithName(categoryNewName); + final RestCategoryModel updatedCategory = restClient.authenticateUser(dataUser.getAdminUser()) + .withCoreAPI() + .usingCategory(notLinkedCategory) + .include(INCLUDE_COUNT_PARAM) + .updateCategory(fixedCategoryModel); + + restClient.assertStatusCodeIs(OK); + updatedCategory.assertThat().field(FIELD_ID).is(notLinkedCategory.getId()); + updatedCategory.assertThat().field(FIELD_COUNT).is(0); + } + + private RestCategoryModelsCollection linkContentToCategories(final RepoTestModel node, final RestCategoryModel... categories) + { + final List categoryLinkModels = Arrays.stream(categories) + .map(RestCategoryModel::getId) + .map(this::createCategoryLinkModelWithId) + .collect(Collectors.toList()); + final RestCategoryModelsCollection linkedCategories = restClient.authenticateUser(user).withCoreAPI().usingNode(node).linkToCategories(categoryLinkModels); + + restClient.assertStatusCodeIs(CREATED); + + return linkedCategories; + } + + private RestCategoryModel prepareCategoryUnderRoot() + { + return prepareCategoryUnder(createCategoryModelWithId(ROOT_CATEGORY_ID)); + } + + private RestCategoryModel prepareCategoryUnder(final RestCategoryModel parentCategory) + { + final RestCategoryModel categoryModel = createCategoryModelWithName(getRandomName(CATEGORY_NAME_PREFIX)); + final RestCategoryModel createdCategory = restClient.authenticateUser(dataUser.getAdminUser()) + .withCoreAPI() + .usingCategory(parentCategory) + .createSingleCategory(categoryModel); + restClient.assertStatusCodeIs(CREATED); + + return createdCategory; + } + + private RestCategoryModel createCategoryModelWithId(final String id) + { + return createCategoryModelWithIdAndName(id, null); + } + + private RestCategoryModel createCategoryModelWithName(final String name) + { + return createCategoryModelWithIdAndName(null, name); + } + + private RestCategoryModel createCategoryModelWithIdAndName(final String id, final String name) + { + return RestCategoryModel.builder() + .id(id) + .name(name) + .create(); + } + + private RestCategoryLinkBodyModel createCategoryLinkModelWithId(final String id) + { + return RestCategoryLinkBodyModel.builder() + .categoryId(id) + .create(); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchGetTagsTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchGetTagsTests.java new file mode 100644 index 000000000..ecf486571 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchGetTagsTests.java @@ -0,0 +1,250 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.alfresco.utility.report.log.Step.STEP; + +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.dataprep.CMISUtil; +import org.alfresco.rest.core.RestWrapper; +import org.alfresco.rest.model.RestTagModel; +import org.alfresco.rest.model.RestTagModelsCollection; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.Utility; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.data.RandomData; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; + +/** + * Tests to verify batch indexing of tags using Elasticsearch. + */ +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +public class ElasticsearchGetTagsTests extends AbstractTestNGSpringContextTests +{ + + @Autowired + private ServerHealth serverHealth; + @Autowired + private DataUser dataUser; + @Autowired + private DataSite dataSite; + @Autowired + private DataContent dataContent; + @Autowired + private RestWrapper restClient; + + private UserModel user; + private SiteModel site; + private RestTagModel apple, banana, pineapple, winegrape, grapefruit, orange; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws InterruptedException + { + serverHealth.isServerReachable(); + serverHealth.assertServerIsOnline(); + + STEP("Create user and site"); + user = dataUser.createRandomTestUser(); + site = dataSite.usingUser(user).createPublicRandomSite(); + final FileModel document = dataContent.usingAdmin().usingSite(site).createContent(CMISUtil.DocumentType.TEXT_PLAIN); + + STEP("Create few tags"); + apple = restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI() + .createSingleTag(RestTagModel.builder().tag(RandomData.getRandomName("apple")).create()); + banana = restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI() + .createSingleTag(RestTagModel.builder().tag(RandomData.getRandomName("banana")).create()); + pineapple = restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI() + .createSingleTag(RestTagModel.builder().tag(RandomData.getRandomName("pineapple")).create()); + winegrape = restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI() + .createSingleTag(RestTagModel.builder().tag(RandomData.getRandomName("winegrape")).create()); + grapefruit = restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI() + .createSingleTag(RestTagModel.builder().tag(RandomData.getRandomName("grapefruit")).create()); + orange = restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI() + .createSingleTag(RestTagModel.builder().tag(RandomData.getRandomName("orange")).create()); + + STEP("Wait for indexing to complete"); + Utility.sleep(500, 60000, () -> restClient.authenticateUser(dataUser.getAdminUser()) + .withParams("where=(tag MATCHES ('oran*'))") + .withCoreAPI() + .getTags() + .assertThat() + .entrySetContains("tag", orange.getTag().toLowerCase())); + + // Step 2: now attach apple to document - tag node is guaranteed to be in ES + STEP("Attach apple tag to document"); + restClient.authenticateUser(user).withCoreAPI().usingResource(document) + .addTag(apple.getTag()); + } + + @AfterClass + public void dataCleanup() + { + STEP("Remove created tags"); + restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI().usingTag(apple).deleteTag(); + restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI().usingTag(banana).deleteTag(); + restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI().usingTag(pineapple).deleteTag(); + restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI().usingTag(winegrape).deleteTag(); + restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI().usingTag(grapefruit).deleteTag(); + restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI().usingTag(orange).deleteTag(); + + STEP("Remove site and user"); + dataSite.usingUser(user).deleteSite(site); + dataUser.deleteUser(user); + } + + /** + * Verify if exact name filter can be applied. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION}) + public void testGetTags_withSingleNameFilter() + { + STEP("Get tags with names filter using EQUALS and expect one item in result"); + final RestTagModelsCollection returnedCollection = restClient.authenticateUser(user) + .withParams("where=(tag='" + apple.getTag() + "')") + .withCoreAPI() + .getTags(); + + restClient.assertStatusCodeIs(HttpStatus.OK); + returnedCollection.assertThat() + .entrySetMatches("tag", Set.of(apple.getTag().toLowerCase())); + } + + /** + * Verify if multiple names can be applied as a filter. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION}) + public void testGetTags_withTwoNameFilters() + { + STEP("Get tags with names filter using IN and expect two items in result"); + final RestTagModelsCollection returnedCollection = restClient.authenticateUser(user) + .withParams("where=(tag IN ('" + apple.getTag() + "', '" + banana.getTag() + "'))") + .withCoreAPI() + .getTags(); + + restClient.assertStatusCodeIs(HttpStatus.OK); + returnedCollection.assertThat() + .entrySetMatches("tag", Set.of(apple.getTag().toLowerCase(), banana.getTag().toLowerCase())); + } + + /** + * Verify if alike name filter can be applied. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION}) + public void testGetTags_whichNamesStartsWithOrphan() + { + STEP("Get tags with names filter using MATCHES and expect one item in result"); + final RestTagModelsCollection returnedCollection = restClient.authenticateUser(user) + .withParams("where=(tag MATCHES ('*an*'))") + .withCoreAPI() + .getTags(); + + restClient.assertStatusCodeIs(HttpStatus.OK); + returnedCollection.assertThat() + .entrySetContains("tag", banana.getTag().toLowerCase(), orange.getTag().toLowerCase()); + } + + /** + * Verify that tags can be filtered by exact name and alike name at the same time. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION}) + public void testGetTags_withExactNameAndAlikeFilters() + { + STEP("Get tags with names filter using EQUALS and MATCHES and expect three items in result"); + final RestTagModelsCollection returnedCollection = restClient.authenticateUser(user) + .withParams("where=(tag='" + orange.getTag() + "' OR tag MATCHES ('*grape*'))") + .withCoreAPI() + .getTags(); + + restClient.assertStatusCodeIs(HttpStatus.OK); + returnedCollection.assertThat() + .entrySetMatches("tag", Set.of(orange.getTag().toLowerCase(), grapefruit.getTag().toLowerCase(), winegrape.getTag().toLowerCase())); + } + + /** + * Verify if multiple alike filters can be applied. + */ + @Test(groups = {TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION}) + public void testGetTags_withTwoAlikeFilters() + { + STEP("Get tags applying names filter using MATCHES twice and expect three items in result"); + final RestTagModelsCollection returnedCollection = restClient.authenticateUser(user) + .withParams("where=(tag MATCHES ('*apple*') OR tag MATCHES ('grape*'))") + .withCoreAPI() + .getTags(); + + restClient.assertStatusCodeIs(HttpStatus.OK); + returnedCollection.assertThat() + .entrySetMatches("tag", Set.of(apple.getTag().toLowerCase(), pineapple.getTag().toLowerCase(), grapefruit.getTag().toLowerCase())); + } + + /** + * Verify that providing incorrect field name in where query will result with 400 (Bad Request). + */ + @Test(groups = {TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION}) + public void testGetTags_withWrongWherePropertyNameAndExpect400() + { + STEP("Try to get tags with names filter using EQUALS and wrong property name and expect 400"); + restClient.authenticateUser(user) + .withParams("where=(name=apple)") + .withCoreAPI() + .getTags(); + + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST) + .assertLastError().containsSummary("Where query error: property with name: name is not expected"); + } + + /** + * Verify tht AND operator is not supported in where query and expect 400 (Bad Request). + */ + @Test(groups = {TestGroup.REST_API, TestGroup.TAGS, TestGroup.REGRESSION}) + public void testGetTags_queryAndOperatorNotSupported() + { + STEP("Try to get tags applying names filter using AND operator and expect 400"); + restClient.authenticateUser(user) + .withParams("where=(tag=apple AND tag IN ('banana', 'melon'))") + .withCoreAPI() + .getTags(); + + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST) + .assertLastError().containsSummary("An invalid WHERE query was received. Unsupported Predicate"); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchIsUnsetTest.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchIsUnsetTest.java new file mode 100644 index 000000000..121d0a1cc --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchIsUnsetTest.java @@ -0,0 +1,266 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; +import static java.util.function.Predicate.not; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Stream.concat; +import static java.util.stream.Stream.of; + +import static org.apache.http.entity.ContentType.APPLICATION_JSON; +import static org.assertj.core.api.Assertions.assertThat; + +import static org.alfresco.elasticsearch.SearchQueryService.req; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import com.google.common.io.CharStreams; +import com.google.gson.Gson; +import org.apache.http.HeaderElement; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.entity.StringEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.dataprep.AlfrescoHttpClient; +import org.alfresco.dataprep.AlfrescoHttpClientFactory; +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.data.DataUser; + +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +public class ElasticsearchIsUnsetTest extends AbstractTestNGSpringContextTests +{ + private static final String CM_TITLED_ASPECT = "cm:titled"; + private static final String CM_TITLE_PROPERTY = "cm:title"; + private static final String CM_DESCRIPTION_PROPERTY = "cm:description"; + + private final Gson gson = new Gson(); + + @Autowired + SearchQueryService searchQueryService; + + @Autowired + private AlfrescoHttpClientFactory alfrescoHttpClientFactory; + + @Autowired + private DataUser dataUser; + + private AlfrescoHttpClient alfrescoClient; + private AlfNode testNode; + + @BeforeClass + public void setUp() throws IOException + { + alfrescoClient = alfrescoHttpClientFactory.getObject(); + testNode = createNodeWithTitleButWithoutDescription(); + } + + @AfterClass + public void tearDown() + { + alfrescoClient.close(); + } + + @Test + public void testElasticsearchIsUnsetQuery() throws Exception + { + assertNodeIsReadyForTesting(); + + // cm:title is set, we should have no result + shouldNotFindNode("ISUNSET:\"" + CM_TITLE_PROPERTY + "\""); + shouldFindNode("NOT ISUNSET:\"" + CM_TITLE_PROPERTY + "\""); + + // cm:description is not set, we should have a result + shouldFindNode("ISUNSET:\"" + CM_DESCRIPTION_PROPERTY + "\""); + shouldNotFindNode("NOT ISUNSET:\"" + CM_DESCRIPTION_PROPERTY + "\""); + + removeCmTitledAspect(); + + // ISUNSET is type aware, no cm:titled aspect so no result + shouldNotFindNode("ISUNSET:\"" + CM_TITLE_PROPERTY + "\""); + shouldNotFindNode("ISUNSET:\"" + CM_DESCRIPTION_PROPERTY + "\""); + shouldFindNode("NOT ISUNSET:\"" + CM_TITLE_PROPERTY + "\""); + shouldFindNode("NOT ISUNSET:\"" + CM_DESCRIPTION_PROPERTY + "\""); + + restoreCmTitledAspect(); + + // cm:titled aspect is back but cm:title property has been "unset" + shouldFindNode("ISUNSET:\"" + CM_TITLE_PROPERTY + "\""); + shouldFindNode("ISUNSET:\"" + CM_DESCRIPTION_PROPERTY + "\""); + shouldNotFindNode("NOT ISUNSET:\"" + CM_TITLE_PROPERTY + "\""); + shouldNotFindNode("NOT ISUNSET:\"" + CM_DESCRIPTION_PROPERTY + "\""); + } + + private void restoreCmTitledAspect() throws IOException + { + setAspects(testNode.getAspects()); + } + + private void removeCmTitledAspect() throws IOException + { + final List noCmTitled = testNode.getAspects().stream().filter(not(CM_TITLED_ASPECT::equals)).toList(); + setAspects(noCmTitled); + } + + private void assertNodeIsReadyForTesting() + { + assertThat(testNode.getAspects()).contains(CM_TITLED_ASPECT); + shouldFindNode(); + } + + private void shouldFindNode(String... conjunctions) + { + searchQueryService.expectResultsInclude(searchByName(conjunctions), dataUser.getAdminUser(), testNode.getName()); + } + + private void shouldNotFindNode(String... conjunctions) + { + searchQueryService.expectNoResultsFromQuery(searchByName(conjunctions), dataUser.getAdminUser()); + } + + private SearchRequest searchByName(String... conjunctions) + { + return req(concat(of("cm:name:\"" + testNode.getName() + "\""), of(conjunctions)).collect(joining(" AND "))); + } + + private void setAspects(Collection aspectsToSet) throws IOException + { + final URI nodeURI = URI.create(alfrescoClient.getApiVersionUrl()).resolve("nodes/" + testNode.getId()); + final HttpPut putRequest = new HttpPut(nodeURI); + + final String jsonBody = toJsonString(Map.of("aspectNames", aspectsToSet)); + putRequest.setEntity(new StringEntity(jsonBody, APPLICATION_JSON)); + + nodeApiRequest(putRequest, 200); + } + + private AlfNode createNodeWithTitleButWithoutDescription() throws IOException + { + final URI parentURI = URI.create(alfrescoClient.getApiVersionUrl()).resolve("nodes/-my-/children"); + final HttpPost postRequest = new HttpPost(parentURI); + final String jsonBody = toJsonString( + Map.of("name", uniqueName(), + "nodeType", "cm:content", + "properties", Map.of("cm:title", "UnsetTestingDocument"))); + postRequest.setEntity(new StringEntity(jsonBody, APPLICATION_JSON)); + + return nodeApiRequest(postRequest, 201); + } + + private AlfNode nodeApiRequest(HttpRequestBase request, int expectedResponseCode) throws IOException + { + final HttpResponse response = alfrescoClient.executeAsAdmin(request); + assertThat(response.getStatusLine().getStatusCode()).isEqualTo(expectedResponseCode); + + final HttpEntity entity = response.getEntity(); + assertThat(entity).isNotNull(); + assertThat(entity.getContentType()).isNotNull(); + assertThat(entity.getContentType().getElements()).hasSize(1); + + final HeaderElement contentTypeHeader = entity.getContentType().getElements()[0]; + assertThat(contentTypeHeader.getName()).isEqualTo(APPLICATION_JSON.getMimeType()); + + final Charset contentCharset = ofNullable(contentTypeHeader.getParameterByName("charset")) + .map(NameValuePair::getValue) + .map(Charset::forName) + .orElse(StandardCharsets.UTF_8); + + try (var reader = new InputStreamReader(entity.getContent(), contentCharset)) + { + return asNode((Map) fromJsonString(CharStreams.toString(reader)).get("entry")); + } + } + + private AlfNode asNode(Map map) + { + return new AlfNode(map); + } + + private String uniqueName() + { + return "Name-" + UUID.randomUUID(); + } + + private String toJsonString(Map map) + { + return gson.toJson(map); + } + + private Map fromJsonString(String string) + { + return gson.fromJson(string, Map.class); + } + + private static class AlfNode + { + private final Map nodeMap; + + private AlfNode(Map nodeMap) + { + this.nodeMap = requireNonNull(nodeMap); + } + + public String getId() + { + return requireNonNull((String) nodeMap.get("id")); + } + + public Set getAspects() + { + return ofNullable(nodeMap.get("aspectNames")) + .map(c -> (Collection) c) + .map(Set::copyOf) + .orElseGet(Set::of); + } + + public String getName() + { + return (String) nodeMap.get("name"); + } + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchLimitTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchLimitTests.java new file mode 100644 index 000000000..407440878 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchLimitTests.java @@ -0,0 +1,139 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.alfresco.elasticsearch.SearchQueryService.req; +import static org.alfresco.tas.TestDataUtility.getAlphabeticUUID; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.search.RestRequestLimitsModel; +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.constants.UserRole; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; + +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", initializers = AlfrescoStackInitializer.class) +/** + * In this test we are verifying the Track Total Hits feature + */ +public class ElasticsearchLimitTests extends AbstractTestNGSpringContextTests +{ + private static final String PREFIX = getAlphabeticUUID() + "_"; + private static final int TOTAL_DOCUMENT_COUNT = 20; + + @Autowired + private DataUser dataUser; + + @Autowired + private DataContent dataContent; + + @Autowired + private DataSite dataSite; + + @Autowired + private ServerHealth serverHealth; + + @Autowired + protected SearchQueryService searchQueryService; + + private UserModel userSite1; + private SiteModel siteModel1; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + serverHealth.assertServerIsOnline(); + + userSite1 = dataUser.createRandomTestUser(); + + siteModel1 = dataSite.usingUser(userSite1).createPrivateRandomSite(); + + dataUser.addUserToSite(userSite1, siteModel1, UserRole.SiteContributor); + + for (int i = 0; i < TOTAL_DOCUMENT_COUNT; i++) + { + createContent(PREFIX + i + ".txt", "Document " + i, siteModel1, userSite1); + } + } + + @Test(groups = TestGroup.SEARCH) + public void searchUsingTotalHitsLimitDefaultValue() + { + SearchRequest request = req("cm:name:" + PREFIX + "*.txt"); + + // Without setting limits, we should get all up to 10k + searchQueryService.expectTotalHitsFromQuery(request, userSite1, TOTAL_DOCUMENT_COUNT); + } + + @Test(groups = TestGroup.SEARCH) + public void searchUsingTotalHitsLimitZeroShouldDefault() + { + SearchRequest request = req("cm:name:" + PREFIX + "*.txt"); + + // Setting trackTotalHitsLimit to 0 should behave as not setting it, counting all up to 10k + request.setLimits(new RestRequestLimitsModel(null, null, 0)); + searchQueryService.expectTotalHitsFromQuery(request, userSite1, TOTAL_DOCUMENT_COUNT); + } + + @Test(groups = TestGroup.SEARCH) + public void searchUsingTotalHitsLimit() + { + SearchRequest request = req("cm:name:" + PREFIX + "*.txt"); + + // Setting trackTotalHitsLimit to 10 should only count up to 10 + request.setLimits(new RestRequestLimitsModel(null, null, 10)); + searchQueryService.expectTotalHitsFromQuery(request, userSite1, 10); + } + + @Test(groups = TestGroup.SEARCH) + public void searchUsingTotalHitsUnlimited() + { + SearchRequest request = req("cm:name:" + PREFIX + "*.txt"); + + // Setting trackTotalHitsLimit to -1 should only count up to max int + request.setLimits(new RestRequestLimitsModel(null, null, -1)); + searchQueryService.expectTotalHitsFromQuery(request, userSite1, TOTAL_DOCUMENT_COUNT); + } + + private FileModel createContent(String filename, String content, SiteModel site, UserModel user) + { + FileModel fileModel = new FileModel(filename, FileType.TEXT_PLAIN, content); + return dataContent.usingUser(user).usingSite(site).createContent(fileModel); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchLiveIndexingTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchLiveIndexingTests.java new file mode 100644 index 000000000..14c806734 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchLiveIndexingTests.java @@ -0,0 +1,263 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static java.util.Arrays.asList; + +import static org.alfresco.elasticsearch.SearchQueryService.req; +import static org.alfresco.tas.TestDataUtility.getAlphabeticUUID; + +import java.util.Map; +import java.util.function.Predicate; + +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.json.simple.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.dataprep.AlfrescoHttpClient; +import org.alfresco.dataprep.AlfrescoHttpClientFactory; +import org.alfresco.rest.search.RestRequestQueryModel; +import org.alfresco.rest.search.SearchNodeModel; +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.constants.UserRole; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; + +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +/** + * In this test we are verifying end-to-end the indexing and search in Elasticsearch. In order to test ACLs we created 2 sites and 3 users. + */ +public class ElasticsearchLiveIndexingTests extends AbstractTestNGSpringContextTests +{ + private static final String PREFIX = getAlphabeticUUID() + "_"; + private static final String UNIQUE_WORD = getAlphabeticUUID(); + private static final String FILE_0_NAME = PREFIX + "test.txt"; + private static final String FILE_1_NAME = PREFIX + "another.txt"; + private static final String FILE_2_NAME = PREFIX + "user1.txt"; + private static final String FILE_3_NAME = PREFIX + "user1Old.txt"; + public static final String BEFORE_1970_TXT = "before1970.txt"; + + @Autowired + private DataUser dataUser; + + @Autowired + private DataContent dataContent; + + @Autowired + private DataSite dataSite; + + @Autowired + private AlfrescoHttpClientFactory alfrescoHttpClientFactory; + + @Autowired + private ServerHealth serverHealth; + + @Autowired + protected SearchQueryService searchQueryService; + + private UserModel userSite1; + private UserModel userSite2; + private UserModel userMultiSite; + private SiteModel siteModel1; + private SiteModel siteModel2; + + /** + * Data will be prepared using the schema below: + *

+ * Site1: - Users: userSite1, userMultiSite - Documents: FILE_0_NAME (owner: userSite1), FILE_1_NAME (owner: userSite1), FILE_3_NAME (owner: userSite2) + *

+ * Site2: - Users: userSite2, userMultiSite - Documents: FILE_2_NAME (owner: userSite2) + */ + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + serverHealth.assertServerIsOnline(); + + userSite1 = dataUser.createRandomTestUser(); + userSite2 = dataUser.createRandomTestUser(); + userMultiSite = dataUser.createRandomTestUser(); + + siteModel1 = dataSite.usingUser(userSite1).createPrivateRandomSite(); + siteModel2 = dataSite.usingUser(userSite2).createPrivateRandomSite(); + + dataUser.addUserToSite(userSite2, siteModel1, UserRole.SiteContributor); + dataUser.addUserToSite(userMultiSite, siteModel1, UserRole.SiteContributor); + dataUser.addUserToSite(userMultiSite, siteModel2, UserRole.SiteContributor); + + createContent(FILE_0_NAME, "This is the first test containing the unique word " + UNIQUE_WORD, siteModel1, userSite1); + createContent(FILE_1_NAME, "This is another TEST file", siteModel1, userSite1); + createContent(FILE_2_NAME, "This is another test file", siteModel2, userSite2); + createContent(FILE_3_NAME, "This is another Test file", siteModel1, userSite2); + // remove the user from site, but he keeps ownership on FILE_3_NAME + dataUser.removeUserFromSite(userSite2, siteModel1); + } + + @Test(groups = TestGroup.SEARCH) + public void searchCanFindAFileUsingIncludeParameter() + { + SearchRequest queryWithoutIncludes = req(UNIQUE_WORD); + Predicate allFieldsNull = searchNodeModel -> searchNodeModel.getProperties() == null + && searchNodeModel.getPath() == null + && searchNodeModel.getAspectNames() == null + && searchNodeModel.getAllowableOperations() == null + && searchNodeModel.getPermissions() == null + && searchNodeModel.getAssociation() == null + && searchNodeModel.isLocked() == null + && searchNodeModel.isLink() == null; + searchQueryService.expectAllResultsFromQuery(queryWithoutIncludes, userSite1, allFieldsNull); + + SearchRequest queryWithIncludes = new SearchRequest(); + // A full list of all fields that can be included is declared in constant: + // org.alfresco.rest.api.search.impl.SearchMapper.PERMITTED_INCLUDES + queryWithIncludes.setInclude(asList("properties", "path", "aspectNames", "isLocked", "allowableOperations", + "permissions", "isLink", "association")); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery(UNIQUE_WORD); + queryWithIncludes.setQuery(queryReq); + Predicate noFieldsNull = searchNodeModel -> searchNodeModel.getProperties() != null + && searchNodeModel.getPath() != null + && searchNodeModel.getAspectNames() != null + && searchNodeModel.getAllowableOperations() != null + && searchNodeModel.getPermissions() != null + && searchNodeModel.getAssociation() != null + && searchNodeModel.isLocked() != null + && searchNodeModel.isLink() != null; + searchQueryService.expectAllResultsFromQuery(queryWithIncludes, userSite1, noFieldsNull); + } + + @Test(groups = TestGroup.SEARCH) + public void searchCanFindAFile() + { + // this test must found only one documents, while documents in the system are four because + // only one contains the unique word. + searchQueryService.expectResultsFromQuery(req(UNIQUE_WORD), userSite1, FILE_0_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void searchCanFindFilesOnASite() + { + searchQueryService.expectResultsFromQuery(req(PREFIX), userSite1, FILE_0_NAME, FILE_1_NAME, FILE_3_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void searchCanFindAFileOnMultipleSitesWithOwner() + { + searchQueryService.expectResultsFromQuery(req(PREFIX), userSite2, FILE_3_NAME, FILE_2_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void searchCanFindAFileOnMultipleSites() + { + searchQueryService.expectResultsFromQuery(req(PREFIX), userMultiSite, FILE_0_NAME, FILE_1_NAME, FILE_3_NAME, FILE_2_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void wildcardWorksInsideQuotes() + { + searchQueryService.expectResultsFromQuery(req("cm:name:\"" + PREFIX + "user1*\""), userMultiSite, FILE_2_NAME, FILE_3_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void wildcardWorksWithoutQuotes() + { + searchQueryService.expectResultsFromQuery(req("cm:name:" + PREFIX + "user1*"), userMultiSite, FILE_2_NAME, FILE_3_NAME); + } + + @Test(groups = TestGroup.SEARCH, enabled = false) // Test should be re-enabled within: ACS-6068 + public void wildcardNodeRefQuery() + { + searchQueryService.expectResultsFromQuery(req("ANCESTOR:\"" + siteModel2.getGuid().substring(0, 10) + "*\""), userMultiSite, "documentLibrary", FILE_2_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void findFileWithRangeQuery() + { + searchQueryService.expectResultsFromQuery(req("cm:created:[NOW-1YEAR TO MAX] AND name:" + FILE_0_NAME), userSite1, FILE_0_NAME); + } + + @Test(groups = TestGroup.SEARCH) + public void omitFileWithRangeQuery() + { + searchQueryService.expectNoResultsFromQuery(req("cm:created:[MIN TO NOW-2YEARS] AND name:" + FILE_0_NAME), userSite1); + } + + @Test(groups = TestGroup.SEARCH) + public void indexAndSearchForDateBefore1970() + { + // Elasticsearch doesn't accept numbers for dates before 1970, so we create and search for a specific document in order to verify that. + createNodeWithProperties(siteModel1, new FileModel(BEFORE_1970_TXT, FileType.TEXT_PLAIN), userSite1, + Map.of("cm:from", -2637887000L)); + + searchQueryService.expectResultsInclude(req("cm:from:1969-12-01T11:15:13Z"), userSite1, BEFORE_1970_TXT); + } + + private FileModel createContent(String filename, String content, SiteModel site, UserModel user) + { + FileModel fileModel = new FileModel(filename, FileType.TEXT_PLAIN, content); + return dataContent.usingUser(user).usingSite(site) + .createContent(fileModel); + } + + private void createNodeWithProperties(SiteModel parentSite, FileModel fileModel, UserModel currentUser, Map properties) + { + AlfrescoHttpClient client = alfrescoHttpClientFactory.getObject(); + String reqUrl = client.getApiVersionUrl() + "nodes/" + parentSite.getGuid() + "/children"; + String name = fileModel.getName(); + + HttpPost post = new HttpPost(reqUrl); + JSONObject body = new JSONObject(); + body.put("name", name); + body.put("nodeType", "cm:content"); + + JSONObject jsonProperties = new JSONObject(); + jsonProperties.putAll(properties); + body.put("properties", jsonProperties); + + post.setEntity(client.setMessageBody(body)); + + // Send Request + logger.info(String.format("POST: '%s'", reqUrl)); + HttpResponse response = client.execute(currentUser.getUsername(), currentUser.getPassword(), post); + if (org.apache.http.HttpStatus.SC_CREATED != response.getStatusLine().getStatusCode()) + { + throw new RuntimeException("Could not create file. Request response: " + client.getParameterFromJSON(response, "briefSummary", "error")); + } + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchPathIndexingTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchPathIndexingTests.java new file mode 100644 index 000000000..157307a7b --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchPathIndexingTests.java @@ -0,0 +1,301 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.alfresco.elasticsearch.SearchQueryService.req; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.model.ContentModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.network.ServerHealth; +import org.alfresco.utility.report.log.Step; + +/** + * Tests to verify indexing of paths using Elasticsearch. + */ +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +@SuppressWarnings({"PMD.JUnitTestsShouldIncludeAssert", "PMD.JUnit4TestShouldUseTestAnnotation"}) // these are testng tests and use searchQueryService.expectResultsFromQuery for assertion +public class ElasticsearchPathIndexingTests extends AbstractTestNGSpringContextTests +{ + @Autowired + private ServerHealth serverHealth; + @Autowired + private DataUser dataUser; + @Autowired + private DataSite dataSite; + @Autowired + private DataContent dataContent; + @Autowired + protected SearchQueryService searchQueryService; + + private org.alfresco.utility.model.UserModel testUser; + + private org.alfresco.utility.model.SiteModel testSite; + + private List testFolders; + + private String testFileName; + private String filenameWhichIncludesWhitespace = "TestFile " + UUID.randomUUID() + ".txt"; + private String testFileNameWithWhitespace; + + /** + * Create a user and a private site containing some nested folders with a document in. + */ + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + serverHealth.isServerReachable(); + serverHealth.assertServerIsOnline(); + + Step.STEP("Create a test user and private site containing three nested folders and a document."); + testUser = dataUser.createRandomTestUser(); + testSite = dataSite.usingUser(testUser).createPrivateRandomSite(); + + testFolders = createNestedFolders(3); + + testFileName = createDocument(testFolders.get(testFolders.size() - 1)); + + testFileNameWithWhitespace = createDocument(testFolders.get(testFolders.size() - 1), filenameWhichIncludesWhitespace); + } + + @Test(groups = TestGroup.SEARCH) + public void testSimple() + { + SearchRequest query = req("PATH:\"//cm:" + testFileName + "\""); + searchQueryService.expectResultsFromQuery(query, testUser, testFileName); + } + + @Test(groups = TestGroup.SEARCH) + public void testRelativePathQuery() + { + SearchRequest query = req("PATH:\"//cm:" + testFileName + "\" "); + searchQueryService.expectResultsFromQuery(query, testUser, testFileName); + } + + @Test(groups = TestGroup.SEARCH) + public void testRelativePathQueryWithoutPrefixes() + { + SearchRequest query = req("PATH:\"//" + testFileName + "\" AND name:*"); + searchQueryService.expectResultsFromQuery(query, testUser, testFileName); + } + + @Test(groups = TestGroup.SEARCH) + public void testWildcardQuery() + { + // The test file should be the only descendent of the last folder. + SearchRequest query = req("PATH:\"//" + testSite.getId() + "//" + testFolders.get(testFolders.size() - 1).getName() + "/*\" AND name:*"); + searchQueryService.expectResultsFromQuery(query, testUser, testFileName, testFileNameWithWhitespace); + } + + @Test(groups = TestGroup.SEARCH) + public void testWildcardQueryWithNamespaces() + { + // The test file should be the only descendent of the last folder. + SearchRequest query = req("PATH:\"//cm:" + testSite.getId() + "//cm:" + testFolders.get(testFolders.size() - 1).getName() + "/*\" AND name:*"); + searchQueryService.expectResultsFromQuery(query, testUser, testFileName, testFileNameWithWhitespace); + } + + @Test(groups = TestGroup.SEARCH) + public void testAbsolutePathQuery() + { + String folderPath = testFolders.stream().map(folder -> "cm:" + folder.getName()).collect(Collectors.joining("/")); + SearchRequest query = req("PATH:\"/app:company_home/st:sites/cm:" + testSite.getId() + "/cm:documentLibrary/" + folderPath + "/cm:" + testFileName + "\" AND cm:name:*"); + searchQueryService.expectResultsFromQuery(query, testUser, testFileName); + } + + @Test(groups = TestGroup.SEARCH) + public void testAbsolutePathQueryWithoutPrefixes() + { + String folderPath = testFolders.stream().map(folder -> folder.getName()).collect(Collectors.joining("/")); + SearchRequest query = req("PATH:\"/company_home/sites/" + testSite.getId() + "/documentLibrary/" + folderPath + "/" + testFileName + "\" AND name:*"); + searchQueryService.expectResultsFromQuery(query, testUser, testFileName); + } + + @Test(groups = TestGroup.SEARCH) + public void testRootNodes() + { + SearchRequest query = req("PATH:\"/*\" AND name:*"); + searchQueryService.expectResultsFromQuery(query, testUser, "categories", "Company Home"); + } + + @Test(groups = TestGroup.SEARCH) + public void testRootNodesWithoutWildcard() + { + SearchRequest query = req("PATH:\"/\""); + searchQueryService.expectNodeTypesFromQuery(query, testUser, "sys:store_root"); + } + + @Test(groups = TestGroup.SEARCH) + public void testPathNameMismatch() + { + SearchRequest query = req("PATH:\"/*\" AND name:" + testFileName + " AND name:*"); + searchQueryService.expectNoResultsFromQuery(query, testUser); + } + + @Test(groups = TestGroup.SEARCH) + public void testPathNameIntersect() + { + SearchRequest query = req("PATH:\"//*\" AND name:" + testFileName + " AND name:*"); + searchQueryService.expectResultsFromQuery(query, testUser, testFileName); + } + + @Test(groups = TestGroup.SEARCH) + public void testAllDescendentsOfFolder() + { + SearchRequest query = req("PATH:\"//" + testFolders.get(0).getName() + "//*\" AND name:*"); + searchQueryService.expectResultsFromQuery(query, testUser, testFileName, testFileNameWithWhitespace, testFolders.get(1).getName(), testFolders.get(2).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testSearchAllDescendentsOfFolderQueryWithNamespaces() + { + SearchRequest query = req("PATH:\"//cm:" + testFolders.get(0).getName() + "//*\" AND name:*"); + searchQueryService.expectResultsFromQuery(query, testUser, testFileName, testFileNameWithWhitespace, testFolders.get(1).getName(), testFolders.get(2).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testWhereFolderIsAncestor() + { + SearchRequest query = req("ANCESTOR:\"" + testFolders.get(2).getNodeRef() + "\" AND name:*"); + searchQueryService.expectResultsFromQuery(query, testUser, testFileName, testFileNameWithWhitespace); + } + + @Test(groups = TestGroup.SEARCH) + public void testAncestorWithWorkspaceReference() + { + SearchRequest query = req("ANCESTOR:\"workspace://SpacesStore/" + testFolders.get(0).getNodeRef() + "\" AND name:*"); + searchQueryService.expectResultsFromQuery(query, testUser, testFileName, testFileNameWithWhitespace, testFolders.get(1).getName(), testFolders.get(2).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testPrimaryParent() + { + SearchRequest query = req("PRIMARYPARENT:\"" + testFolders.get(1).getNodeRef() + "\" AND name:*"); + searchQueryService.expectResultsFromQuery(query, testUser, testFolders.get(2).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testParent() + { + SearchRequest query = req("PARENT:\"" + testFolders.get(1).getNodeRef() + "\" AND name:*"); + searchQueryService.expectResultsFromQuery(query, testUser, testFolders.get(2).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testAllFoldersInSite() + { + SearchRequest query = req("PATH:\"/*/sites/" + testSite.getId() + "/*//*\" AND TYPE:\"cm:folder\" AND name:*"); + String[] folderNames = testFolders.stream().map(ContentModel::getName).toArray(String[]::new); + searchQueryService.expectResultsFromQuery(query, testUser, folderNames); + } + + @Test(groups = TestGroup.SEARCH) + public void testSearchAllFoldersInSiteQueryWithNamespaces() + { + SearchRequest query = req("PATH:\"/*/st:sites/cm:" + testSite.getId() + "/*//*\" AND TYPE:\"cm:folder\" AND name:*"); + String[] folderNames = testFolders.stream().map(ContentModel::getName).toArray(String[]::new); + searchQueryService.expectResultsFromQuery(query, testUser, folderNames); + } + + @Test(groups = TestGroup.SEARCH, enabled = false) + public void testUpdatePath() + { + // disabled test: find a way to rename or move a folder on repository + String folderPath = testFolders.stream().map(folder -> "cm:" + folder.getName()).collect(Collectors.joining("/")); + SearchRequest query = req("PATH:\"/app:company_home/cm:" + testFileName + "\" AND cm:name:*"); + searchQueryService.expectResultsFromQuery(query, testUser, testFileName); + } + + /** + * Create a set of nested folders in the test site using the test user. + * + * @return The folder objects (containing the randomly generated names) in order of depth. + */ + private List createNestedFolders(int maxDepth) + { + List folders = new ArrayList<>(); + dataContent.usingSite(testSite); + for (int depth = 0; depth < maxDepth; depth++) + { + String folderName = "TestFolder" + depth + "_" + UUID.randomUUID(); + FolderModel folderModel = new FolderModel(folderName); + folders.add(folderModel); + if (depth != 0) + { + dataContent.usingResource(folders.get(depth - 1)); + } + dataContent.usingUser(testUser) + .createFolder(folderModel); + } + return folders; + } + + /** + * Create a document in the given folder using the test user and a random filename. + * + * @param folderModel + * The location to create the document. + * @return The randomly generated name of the new document. + */ + private String createDocument(FolderModel folderModel) + { + return createDocument(folderModel, "TestFile" + UUID.randomUUID() + ".txt"); + } + + /** + * Create a document in the given folder using the test user and the given filename. + * + * @param folderModel + * The location to create the document. + * @param filename + * the filename. + * @return the passed filename. + */ + private String createDocument(FolderModel folderModel, String filename) + { + dataContent.usingUser(testUser) + .usingResource(folderModel) + .createContent(new org.alfresco.utility.model.FileModel(filename, org.alfresco.utility.model.FileType.TEXT_PLAIN, "content")); + return filename; + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchPlainHighlightingTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchPlainHighlightingTests.java new file mode 100644 index 000000000..1650023e3 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchPlainHighlightingTests.java @@ -0,0 +1,478 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static java.util.stream.Collectors.toSet; + +import static org.alfresco.elasticsearch.SearchQueryService.req; +import static org.alfresco.utility.report.log.Step.STEP; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.search.ResponseHighlightModel; +import org.alfresco.rest.search.RestRequestFieldsModel; +import org.alfresco.rest.search.RestRequestHighlightModel; +import org.alfresco.rest.search.SearchNodeModel; +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; + +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +// These are TestNG tests and the assertions are hidden in searchQueryService. +@SuppressWarnings({"PMD.JUnitTestsShouldIncludeAssert"}) +public class ElasticsearchPlainHighlightingTests extends AbstractTestNGSpringContextTests +{ + static final Logger LOGGER = LoggerFactory.getLogger(ElasticsearchPlainHighlightingTests.class); + static final String FILE_A = "fileA.txt"; + static final String FILE_B = "fileB.txt"; + static final String FILE_C = "fileC.txt"; + static final String CONTENT_A = "The quick brown fox jumps over the lazy dog."; + static final String CONTENT_B = """ + The lazy dog sleeps under the quick brown fox. The middle of the document is quite long: + Lorem ipsum dolor sit amet. In veniam tempore hic provident sunt et distinctio velit et reprehenderit + officiis id sapiente omnis et quisquam aliquam et porro perferendis. In sequi placeat ut quaerat voluptatem + ea consequuntur impedit aut eaque enim aut atque rerum. + The end of the document mentions the dog again! + """; + static final String CONTENT_C = """ + The rabbit made a ring in the center of the field. He said to the wolf, "Now, you dance around this ring, and sing just as I do." + Rabbit made a larger ring for himself and danced around just beyond the wolf. + The wolf thought that this was the finest dance he had ever seen. + He and the rabbit danced faster and faster, and sang louder and louder. + As the rabbit danced, he moved nearer and nearer to the edge of the field. + The wolf was dancing so fast and singing so loud that he did not notice this. + At last, Brother Rabbit reached the edge of the field; then he jumped into the blackberry bushes and ran away. + The wolf tried to give chase, but he was so dizzy that he could not run. And the rabbit got away without having his ears cut off. + """; + + @Autowired + ServerHealth serverHealth; + @Autowired + DataUser dataUser; + @Autowired + DataContent dataContent; + @Autowired + DataSite dataSite; + @Autowired + SearchQueryService searchQueryService; + + UserModel user; + SiteModel site; + FileModel fileA; + FileModel fileB; + FileModel fileC; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + serverHealth.assertServerIsOnline(); + + STEP("Create a test user and a private site."); + user = dataUser.createRandomTestUser(); + site = dataSite.usingUser(user).createPrivateRandomSite(); + + STEP("Create two test files with highlightable content"); + FileModel fileModelA = new FileModel(FILE_A, FileType.TEXT_PLAIN, CONTENT_A); + fileA = dataContent.usingUser(user).usingSite(site).createContent(fileModelA); + FileModel fileModelB = new FileModel(FILE_B, FileType.TEXT_PLAIN, CONTENT_B); + fileB = dataContent.usingUser(user).usingSite(site).createContent(fileModelB); + FileModel fileModelC = new FileModel(FILE_C, FileType.TEXT_PLAIN, CONTENT_C); + fileC = dataContent.usingUser(user).usingSite(site).createContent(fileModelC); + } + + @AfterClass + public void dataCleanup() + { + STEP("Remove test site and user"); + dataSite.usingAdmin().deleteSite(site); + dataUser.usingAdmin().deleteUser(user); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testHighlightsInContent() + { + STEP("Search for files mentioning 'dog'"); + String query = "cm:content:dog AND SITE:" + site.getId(); + SearchRequest searchRequest = req("afts", query); + RestRequestHighlightModel highlightModel = RestRequestHighlightModel.builder().fields(List.of("cm:content")).build(); + searchRequest.setHighlight(highlightModel); + // Configure the expected highlights for each document. + Predicate assertionMethod = highlightAssert( + Map.of(FILE_A, Map.of("cm:content", List.of("The quick brown fox jumps over the lazy dog.")), + FILE_B, Map.of("cm:content", List.of("The lazy dog sleeps under the quick brown fox. The middle of the document is quite long:\nLorem", + "\nea consequuntur impedit aut eaque enim aut atque rerum.\nThe end of the document mentions the dog again!\n")))); + searchQueryService.expectAllResultsFromQuery(searchRequest, user, assertionMethod); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testHighlightsInTwoFields() + { + STEP("Search for files with 'file' in the name that mention 'middle'"); + String query = "cm:content:middle AND cm:name:file AND SITE:" + site.getId(); + SearchRequest searchRequest = req("afts", query); + RestRequestHighlightModel highlightModel = RestRequestHighlightModel.builder().fields(List.of("cm:name", "cm:content")).build(); + searchRequest.setHighlight(highlightModel); + // Configure the expected highlights for each document. + Predicate assertionMethod = highlightAssert( + Map.of(FILE_B, Map.of("cm:name", List.of("fileB.txt"), + "cm:content", List.of("The lazy dog sleeps under the quick brown fox. The middle of the document is quite long:\nLorem")))); + searchQueryService.expectAllResultsFromQuery(searchRequest, user, assertionMethod); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testHighlightsWithCustomPrefixAndPostfix() + { + STEP("Search for files mentioning 'dog' with custom prefix and postfix"); + String query = "cm:content:dog AND SITE:" + site.getId(); + SearchRequest searchRequest = req("afts", query); + RestRequestHighlightModel highlightModel = RestRequestHighlightModel.builder() + .prefix("") + .postfix("") + .fields(List.of("cm:content")) + .build(); + searchRequest.setHighlight(highlightModel); + // Configure the expected highlights for each document. + Predicate assertionMethod = highlightAssert( + Map.of(FILE_A, Map.of("cm:content", List.of("The quick brown fox jumps over the lazy dog.")), + FILE_B, Map.of("cm:content", List.of("The lazy dog sleeps under the quick brown fox. The middle of the document is quite long:\nLorem", + "\nea consequuntur impedit aut eaque enim aut atque rerum.\nThe end of the document mentions the dog again!\n")))); + searchQueryService.expectAllResultsFromQuery(searchRequest, user, assertionMethod); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testHighlightsWithBlankPrefixAndPostfix() + { + STEP("Search for files mentioning 'middle' with blank mark as custom prefix and postfix"); + String query = "cm:content:middle AND SITE:" + site.getId(); + SearchRequest searchRequest = req("afts", query); + RestRequestHighlightModel highlightModel = RestRequestHighlightModel.builder() + .prefix(" ") + .postfix(" ") + .fields(List.of("cm:content")) + .build(); + searchRequest.setHighlight(highlightModel); + // Configure the expected highlights for each document. + Predicate assertionMethod = highlightAssert( + Map.of(FILE_B, Map.of("cm:content", List.of("The lazy dog sleeps under the quick brown fox. The middle of the document is quite long:\nLorem")))); + searchQueryService.expectAllResultsFromQuery(searchRequest, user, assertionMethod); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testHighlightsWithGeneralAndFieldSpecificPrefixesAndPostfixes() + { + STEP("Search for files with 'file' in the name that mention 'dog' with general and field specific prefixes or postfixes"); + String query = "cm:content:dog AND cm:name:file AND SITE:" + site.getId(); + SearchRequest searchRequest = req("afts", query); + RestRequestHighlightModel highlightModel = RestRequestHighlightModel.builder() + .prefix("¿") + .postfix("?") + .fields( + RestRequestFieldsModel.of("cm:name"), + RestRequestFieldsModel.of("cm:content", "(", ")")) + .build(); + searchRequest.setHighlight(highlightModel); + // Configure the expected highlights for each document. + Predicate assertionMethod = highlightAssert( + Map.of(FILE_A, Map.of("cm:name", List.of("¿file?A.txt"), "cm:content", List.of("The quick brown fox jumps over the lazy (dog).")), + FILE_B, Map.of("cm:name", List.of("¿file?B.txt"), "cm:content", List.of("The lazy (dog) sleeps under the quick brown fox. The middle of the document is quite long:\nLorem", + "\nea consequuntur impedit aut eaque enim aut atque rerum.\nThe end of the document mentions the (dog) again!\n")))); + searchQueryService.expectAllResultsFromQuery(searchRequest, user, assertionMethod); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testHighlightsWithOneFieldSpecificPrefixAndPostfix() + { + STEP("Search for files with 'file' in the name that mention 'dog' with one field specific prefix or postfix"); + String query = "cm:content:dog AND cm:name:file AND SITE:" + site.getId(); + SearchRequest searchRequest = req("afts", query); + RestRequestHighlightModel highlightModel = RestRequestHighlightModel.builder() + .fields( + RestRequestFieldsModel.of("cm:name"), + RestRequestFieldsModel.of("cm:content", "(", ")")) + .build(); + searchRequest.setHighlight(highlightModel); + // Configure the expected highlights for each document. + Predicate assertionMethod = highlightAssert( + Map.of(FILE_A, Map.of("cm:name", List.of("fileA.txt"), "cm:content", List.of("The quick brown fox jumps over the lazy (dog).")), + FILE_B, Map.of("cm:name", List.of("fileB.txt"), "cm:content", List.of("The lazy (dog) sleeps under the quick brown fox. The middle of the document is quite long:\nLorem", + "\nea consequuntur impedit aut eaque enim aut atque rerum.\nThe end of the document mentions the (dog) again!\n")))); + searchQueryService.expectAllResultsFromQuery(searchRequest, user, assertionMethod); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testHighlightsInTwoFieldsWithPrefixAndWithoutPostfix() + { + STEP("Search for files with 'file' in the name that mention 'middle' with custom prefix"); + String query = "cm:content:middle AND cm:name:file AND SITE:" + site.getId(); + SearchRequest searchRequest = req("afts", query); + RestRequestHighlightModel highlightModel = RestRequestHighlightModel.builder() + .fields(List.of("cm:name", "cm:content")) + .prefix("(") + .build(); + searchRequest.setHighlight(highlightModel); + // Configure the expected highlights for each document. + Predicate assertionMethod = highlightAssert( + Map.of(FILE_B, Map.of("cm:name", List.of("(fileB.txt"), + "cm:content", List.of("The lazy dog sleeps under the quick brown fox. The (middle of the document is quite long:\nLorem")))); + searchQueryService.expectAllResultsFromQuery(searchRequest, user, assertionMethod); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testHighlightsInTwoFieldsWithoutPrefixAndWithPostfix() + { + STEP("Search for files with 'file' in the name that mention 'middle' with custom postfix"); + String query = "cm:content:middle AND cm:name:file AND SITE:" + site.getId(); + SearchRequest searchRequest = req("afts", query); + RestRequestHighlightModel highlightModel = RestRequestHighlightModel.builder() + .fields(List.of("cm:name", "cm:content")) + .postfix(")") + .build(); + searchRequest.setHighlight(highlightModel); + // Configure the expected highlights for each document. + Predicate assertionMethod = highlightAssert( + Map.of(FILE_B, Map.of("cm:name", List.of("file)B.txt"), + "cm:content", List.of("The lazy dog sleeps under the quick brown fox. The middle) of the document is quite long:\nLorem")))); + searchQueryService.expectAllResultsFromQuery(searchRequest, user, assertionMethod); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testHighlightsInContentWithGeneralSnippetCount() + { + STEP("Search for files mentioning 'rabbit' and expect 2 snippets in result"); + String query = "cm:content:rabbit AND SITE:" + site.getId(); + SearchRequest searchRequest = req("afts", query); + RestRequestHighlightModel highlightModel = RestRequestHighlightModel.builder() + .fields(RestRequestFieldsModel.of("cm:content")) + .snippetCount(2) + .build(); + searchRequest.setHighlight(highlightModel); + // Configure the expected highlights for each document. + Predicate assertionMethod = highlightAssert( + Map.of(FILE_C, Map.of("cm:content", List.of("The rabbit made a ring in the center of the field. He said to the wolf, \"Now, you dance around", + " this ring, and sing just as I do.\"\nRabbit made a larger ring for himself and danced around just beyond")))); + searchQueryService.expectAllResultsFromQuery(searchRequest, user, assertionMethod); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testHighlightsInContentWithFieldSpecificSnippetCount() + { + STEP("Search for files mentioning 'rabbit' and expect 3 snippets in result"); + String query = "cm:content:rabbit AND SITE:" + site.getId(); + SearchRequest searchRequest = req("afts", query); + RestRequestHighlightModel highlightModel = RestRequestHighlightModel.builder() + .fields(RestRequestFieldsModel.builder().field("cm:content").snippetCount(3).build()) + .snippetCount(1) + .build(); + searchRequest.setHighlight(highlightModel); + // Configure the expected highlights for each document. + Predicate assertionMethod = highlightAssert( + Map.of(FILE_C, Map.of("cm:content", List.of("The rabbit made a ring in the center of the field. He said to the wolf, \"Now, you dance around", + " this ring, and sing just as I do.\"\nRabbit made a larger ring for himself and danced around just beyond", + " the wolf.\nThe wolf thought that this was the finest dance he had ever seen.\nHe and the rabbit danced")))); + searchQueryService.expectAllResultsFromQuery(searchRequest, user, assertionMethod); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testHighlightsInContentWithZeroSnippetCount() + { + STEP("Search for files mentioning 'rabbit' and expect default 5 snippets in result"); + String query = "cm:content:rabbit AND SITE:" + site.getId(); + SearchRequest searchRequest = req("afts", query); + RestRequestHighlightModel highlightModel = RestRequestHighlightModel.builder() + .fields(RestRequestFieldsModel.of("cm:content")) + .snippetCount(0) + .build(); + searchRequest.setHighlight(highlightModel); + // Configure the expected highlights for each document. + Predicate assertionMethod = highlightAssert( + Map.of(FILE_C, Map.of("cm:content", List.of("The rabbit made a ring in the center of the field. He said to the wolf, \"Now, you dance around", + " this ring, and sing just as I do.\"\nRabbit made a larger ring for himself and danced around just beyond", + " the wolf.\nThe wolf thought that this was the finest dance he had ever seen.\nHe and the rabbit danced", + " faster and faster, and sang louder and louder.\nAs the rabbit danced, he moved nearer and nearer", + " this.\nAt last, Brother Rabbit reached the edge of the field; then he jumped into the blackberry bushes")))); + searchQueryService.expectAllResultsFromQuery(searchRequest, user, assertionMethod); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testHighlightsInContentWithNegativeSnippetCount() + { + STEP("Search for files mentioning 'rabbit' and expect default 5 snippets in result"); + String query = "cm:content:rabbit AND SITE:" + site.getId(); + SearchRequest searchRequest = req("afts", query); + RestRequestHighlightModel highlightModel = RestRequestHighlightModel.builder() + .fields(RestRequestFieldsModel.of("cm:content")) + .snippetCount(-1) + .build(); + searchRequest.setHighlight(highlightModel); + // Configure the expected highlights for each document. + Predicate assertionMethod = highlightAssert( + Map.of(FILE_C, Map.of("cm:content", List.of("The rabbit made a ring in the center of the field. He said to the wolf, \"Now, you dance around", + " this ring, and sing just as I do.\"\nRabbit made a larger ring for himself and danced around just beyond", + " the wolf.\nThe wolf thought that this was the finest dance he had ever seen.\nHe and the rabbit danced", + " faster and faster, and sang louder and louder.\nAs the rabbit danced, he moved nearer and nearer", + " this.\nAt last, Brother Rabbit reached the edge of the field; then he jumped into the blackberry bushes")))); + searchQueryService.expectAllResultsFromQuery(searchRequest, user, assertionMethod); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testHighlightsInContentWithGeneralFragmentSize() + { + STEP("Search for files mentioning 'dog' and expect snippets about 10 characters long"); + String query = "cm:content:dog AND SITE:" + site.getId(); + SearchRequest searchRequest = req("afts", query); + RestRequestHighlightModel highlightModel = RestRequestHighlightModel.builder() + .fields(RestRequestFieldsModel.of("cm:content")) + .fragmentSize(10) + .build(); + searchRequest.setHighlight(highlightModel); + // Configure the expected highlights for each document. + Predicate assertionMethod = highlightAssert( + Map.of(FILE_A, Map.of("cm:content", List.of(" over the lazy dog.")), + FILE_B, Map.of("cm:content", List.of(" dog sleeps", " the dog again!\n")))); + searchQueryService.expectAllResultsFromQuery(searchRequest, user, assertionMethod); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testHighlightsInContentWithFieldSpecificFragmentSize() + { + STEP("Search for files mentioning 'dog' and expect snippets about 15 characters long"); + String query = "cm:content:dog AND SITE:" + site.getId(); + SearchRequest searchRequest = req("afts", query); + RestRequestHighlightModel highlightModel = RestRequestHighlightModel.builder() + .fields(RestRequestFieldsModel.builder().field("cm:content").fragmentSize(15).build()) + .fragmentSize(10) + .build(); + searchRequest.setHighlight(highlightModel); + // Configure the expected highlights for each document. + Predicate assertionMethod = highlightAssert( + Map.of(FILE_A, Map.of("cm:content", List.of(" over the lazy dog.")), + FILE_B, Map.of("cm:content", List.of("The lazy dog", " the dog again!\n")))); + searchQueryService.expectAllResultsFromQuery(searchRequest, user, assertionMethod); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testHighlightsInContentWithZeroFragmentSize() + { + STEP("Search for files mentioning 'dog' and expect snippets having default characters length"); + String query = "cm:content:dog AND SITE:" + site.getId(); + SearchRequest searchRequest = req("afts", query); + RestRequestHighlightModel highlightModel = RestRequestHighlightModel.builder() + .fields(RestRequestFieldsModel.of("cm:content")) + .fragmentSize(0) + .build(); + searchRequest.setHighlight(highlightModel); + // Configure the expected highlights for each document. + Predicate assertionMethod = highlightAssert( + Map.of(FILE_A, Map.of("cm:content", List.of("The quick brown fox jumps over the lazy dog.")), + FILE_B, Map.of("cm:content", List.of("The lazy dog sleeps under the quick brown fox. The middle of the document is quite long:\nLorem", + "\nea consequuntur impedit aut eaque enim aut atque rerum.\nThe end of the document mentions the dog again!\n")))); + searchQueryService.expectAllResultsFromQuery(searchRequest, user, assertionMethod); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testHighlightsInContentWithNegativeFragmentSize() + { + STEP("Search for files mentioning 'dog' and expect snippets having default characters length"); + String query = "cm:content:dog AND SITE:" + site.getId(); + SearchRequest searchRequest = req("afts", query); + RestRequestHighlightModel highlightModel = RestRequestHighlightModel.builder() + .fields(RestRequestFieldsModel.of("cm:content")) + .fragmentSize(-1) + .build(); + searchRequest.setHighlight(highlightModel); + // Configure the expected highlights for each document. + Predicate assertionMethod = highlightAssert( + Map.of(FILE_A, Map.of("cm:content", List.of("The quick brown fox jumps over the lazy dog.")), + FILE_B, Map.of("cm:content", List.of("The lazy dog sleeps under the quick brown fox. The middle of the document is quite long:\nLorem", + "\nea consequuntur impedit aut eaque enim aut atque rerum.\nThe end of the document mentions the dog again!\n")))); + searchQueryService.expectAllResultsFromQuery(searchRequest, user, assertionMethod); + } + + /** + * Create a predicate that returns true if the highlights for a received document match the given expectation. + * + * @param allExpectedHighlights + * The expected highlights for all documents keyed by document name and then field name. + * @return The predicate. + */ + private Predicate highlightAssert(Map>> allExpectedHighlights) + { + return document -> { + if (!allExpectedHighlights.containsKey(document.getName())) + { + LOGGER.error("Unexpected entry in results: {}", document.getName()); + return false; + } + Map> expectedHighlights = allExpectedHighlights.get(document.getName()); + List actualHighlights = document.getSearch().getHighlight(); + Set actualFields = actualHighlights.stream() + .map(ResponseHighlightModel::getField) + .collect(toSet()); + if (!actualFields.equals(expectedHighlights.keySet())) + { + LOGGER.error("Unexpected field set for {}: {}", document.getName(), actualFields); + return false; + } + Set expectedHighlightResponse = new HashSet<>(); + for (String expectedField : expectedHighlights.keySet()) + { + ResponseHighlightModel expectedHighlight = new ResponseHighlightModel(); + List expectedSnippets = expectedHighlights.get(expectedField); + expectedHighlight.setField(expectedField); + expectedHighlight.setSnippets(expectedSnippets); + expectedHighlightResponse.add(expectedHighlight); + } + if (!new HashSet<>(actualHighlights).equals(expectedHighlightResponse)) + { + LOGGER.error("Unexpected highlights for {}, {}", document.getName(), actualHighlights); + return false; + } + return true; + }; + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchProximitySearchTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchProximitySearchTests.java new file mode 100644 index 000000000..0872c9651 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchProximitySearchTests.java @@ -0,0 +1,253 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static java.lang.String.format; + +import static org.alfresco.elasticsearch.SearchQueryService.req; + +import java.util.List; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.model.ContentModel; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; + +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +public class ElasticsearchProximitySearchTests extends AbstractTestNGSpringContextTests +{ + @Autowired + ServerHealth serverHealth; + + @Autowired + DataUser dataUser; + + @Autowired + DataContent dataContent; + + @Autowired + SearchQueryService searchQueryService; + + private UserModel testUser; + private String contentA; + private String contentB; + private String contentC; + private String contentD; + private String nameA; + private String nameB; + private String nameC; + private String nameD; + private String file_NameA_ContentABCD; + private String file_NameAB_ContentABC; + private String file_NameABC_ContentAB; + private String file_NameABCD_ContentA; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + serverHealth.assertServerIsOnline(); + + nameA = uniqueString(); + nameB = uniqueString(); + nameC = uniqueString(); + nameD = uniqueString(); + + contentA = uniqueString(); + contentB = uniqueString(); + contentC = uniqueString(); + contentD = uniqueString(); + + file_NameA_ContentABCD = createContent(join(nameA), join(contentA, contentB, contentC, contentD)); + file_NameAB_ContentABC = createContent(join(nameA, nameB), join(contentA, contentB, contentC)); + file_NameABC_ContentAB = createContent(join(nameA, nameB, nameC), join(contentA, contentB)); + file_NameABCD_ContentA = createContent(join(nameA, nameB, nameC, nameD), join(contentA)); + + testUser = dataUser.createRandomTestUser(); + } + + @Test(groups = {TestGroup.SEARCH, TestGroup.TAGS, TestGroup.REGRESSION}) + public void testProximitySearchUsingAFTSSyntax() + { + final String AFTS = "%s *%s %s"; + + assertAFTS(format(AFTS, contentA, "(0)", contentB), file_NameA_ContentABCD, file_NameAB_ContentABC, file_NameABC_ContentAB); + assertAFTS(format(AFTS, contentA, "(1)", contentB), file_NameA_ContentABCD, file_NameAB_ContentABC, file_NameABC_ContentAB); + assertAFTS(format(AFTS, contentA, "(2)", contentB), file_NameA_ContentABCD, file_NameAB_ContentABC, file_NameABC_ContentAB); + assertAFTS(format(AFTS, contentA, "(3)", contentB), file_NameA_ContentABCD, file_NameAB_ContentABC, file_NameABC_ContentAB); + assertAFTS(format(AFTS, contentA, "", contentB), file_NameA_ContentABCD, file_NameAB_ContentABC, file_NameABC_ContentAB); + + assertAFTS(format(AFTS, contentA, "(0)", contentC)); + assertAFTS(format(AFTS, contentA, "(1)", contentC), file_NameA_ContentABCD, file_NameAB_ContentABC); + assertAFTS(format(AFTS, contentA, "(2)", contentC), file_NameA_ContentABCD, file_NameAB_ContentABC); + assertAFTS(format(AFTS, contentA, "(3)", contentC), file_NameA_ContentABCD, file_NameAB_ContentABC); + assertAFTS(format(AFTS, contentA, "", contentC), file_NameA_ContentABCD, file_NameAB_ContentABC); + + assertAFTS(format(AFTS, contentA, "(0)", contentD)); + assertAFTS(format(AFTS, contentA, "(1)", contentD)); + assertAFTS(format(AFTS, contentA, "(2)", contentD), file_NameA_ContentABCD); + assertAFTS(format(AFTS, contentA, "(3)", contentD), file_NameA_ContentABCD); + assertAFTS(format(AFTS, contentA, "", contentD), file_NameA_ContentABCD); + } + + @Test(groups = {TestGroup.SEARCH, TestGroup.TAGS, TestGroup.REGRESSION}) + public void testProximitySearchUsingAFTSSyntaxForSpecificProperty() + { + final String AFTS = "cm:name:(%s *%s %s)"; + + assertAFTS(format(AFTS, nameA, "(0)", nameB), file_NameAB_ContentABC, file_NameABC_ContentAB, file_NameABCD_ContentA); + assertAFTS(format(AFTS, nameA, "(1)", nameB), file_NameAB_ContentABC, file_NameABC_ContentAB, file_NameABCD_ContentA); + assertAFTS(format(AFTS, nameA, "(2)", nameB), file_NameAB_ContentABC, file_NameABC_ContentAB, file_NameABCD_ContentA); + assertAFTS(format(AFTS, nameA, "(3)", nameB), file_NameAB_ContentABC, file_NameABC_ContentAB, file_NameABCD_ContentA); + assertAFTS(format(AFTS, nameA, "", nameB), file_NameAB_ContentABC, file_NameABC_ContentAB, file_NameABCD_ContentA); + + assertAFTS(format(AFTS, nameA, "(0)", nameC)); + assertAFTS(format(AFTS, nameA, "(1)", nameC), file_NameABC_ContentAB, file_NameABCD_ContentA); + assertAFTS(format(AFTS, nameA, "(2)", nameC), file_NameABC_ContentAB, file_NameABCD_ContentA); + assertAFTS(format(AFTS, nameA, "(3)", nameC), file_NameABC_ContentAB, file_NameABCD_ContentA); + assertAFTS(format(AFTS, nameA, "", nameC), file_NameABC_ContentAB, file_NameABCD_ContentA); + + assertAFTS(format(AFTS, nameA, "(0)", nameD)); + assertAFTS(format(AFTS, nameA, "(1)", nameD)); + assertAFTS(format(AFTS, nameA, "(2)", nameD), file_NameABCD_ContentA); + assertAFTS(format(AFTS, nameA, "(3)", nameD), file_NameABCD_ContentA); + assertAFTS(format(AFTS, nameA, "", nameD), file_NameABCD_ContentA); + } + + @Test(groups = {TestGroup.SEARCH, TestGroup.TAGS, TestGroup.REGRESSION}) + public void testProximitySearchUsingLuceneSyntax() + { + final String lucene = "\"%s %s\"~%d"; + + assertLucene(format(lucene, contentA, contentB, 0), file_NameA_ContentABCD, file_NameAB_ContentABC, file_NameABC_ContentAB); + assertLucene(format(lucene, contentA, contentB, 1), file_NameA_ContentABCD, file_NameAB_ContentABC, file_NameABC_ContentAB); + assertLucene(format(lucene, contentA, contentB, 2), file_NameA_ContentABCD, file_NameAB_ContentABC, file_NameABC_ContentAB); + assertLucene(format(lucene, contentA, contentB, 3), file_NameA_ContentABCD, file_NameAB_ContentABC, file_NameABC_ContentAB); + + assertLucene(format(lucene, contentA, contentC, 0)); + assertLucene(format(lucene, contentA, contentC, 1), file_NameA_ContentABCD, file_NameAB_ContentABC); + assertLucene(format(lucene, contentA, contentC, 2), file_NameA_ContentABCD, file_NameAB_ContentABC); + assertLucene(format(lucene, contentA, contentC, 3), file_NameA_ContentABCD, file_NameAB_ContentABC); + + assertLucene(format(lucene, contentA, contentD, 0)); + assertLucene(format(lucene, contentA, contentD, 1)); + assertLucene(format(lucene, contentA, contentD, 2), file_NameA_ContentABCD); + assertLucene(format(lucene, contentA, contentD, 3), file_NameA_ContentABCD); + } + + @Test(groups = {TestGroup.SEARCH, TestGroup.TAGS, TestGroup.REGRESSION}) + public void testProximitySearchUsingLuceneSyntaxForSpecificProperty() + { + final String lucene = "@cm\\:name:\"%s %s\"~%d"; + + assertLucene(format(lucene, nameA, nameB, 0), file_NameAB_ContentABC, file_NameABC_ContentAB, file_NameABCD_ContentA); + assertLucene(format(lucene, nameA, nameB, 1), file_NameAB_ContentABC, file_NameABC_ContentAB, file_NameABCD_ContentA); + assertLucene(format(lucene, nameA, nameB, 2), file_NameAB_ContentABC, file_NameABC_ContentAB, file_NameABCD_ContentA); + assertLucene(format(lucene, nameA, nameB, 3), file_NameAB_ContentABC, file_NameABC_ContentAB, file_NameABCD_ContentA); + + assertLucene(format(lucene, nameA, nameC, 0)); + assertLucene(format(lucene, nameA, nameC, 1), file_NameABC_ContentAB, file_NameABCD_ContentA); + assertLucene(format(lucene, nameA, nameC, 2), file_NameABC_ContentAB, file_NameABCD_ContentA); + assertLucene(format(lucene, nameA, nameC, 3), file_NameABC_ContentAB, file_NameABCD_ContentA); + + assertLucene(format(lucene, nameA, nameD, 0)); + assertLucene(format(lucene, nameA, nameD, 1)); + assertLucene(format(lucene, nameA, nameD, 2), file_NameABCD_ContentA); + assertLucene(format(lucene, nameA, nameD, 3), file_NameABCD_ContentA); + } + + private void assertLucene(String query, String... expected) + { + assertQueryResult("lucene", query, expected); + } + + private void assertAFTS(String query, String... expected) + { + assertQueryResult("afts", query, expected); + } + + private void assertQueryResult(String language, String query, String... expected) + { + final SearchRequest searchRequest = req(language, query); + if (expected.length == 0) + { + searchQueryService.expectNoResultsFromQuery(searchRequest, testUser); + } + else + { + searchQueryService.expectNodeRefsFromQuery(searchRequest, testUser, expected); + } + } + + private String createContent(String filename, String content) + { + return dataContent + .usingAdmin() + .usingResource(contentRoot()) + .createContent(new FileModel(filename, FileType.TEXT_PLAIN, content)) + .getNodeRef(); + } + + private static ContentModel contentRoot() + { + final ContentModel root = new ContentModel("-root-"); + root.setNodeRef(root.getName()); + return root; + } + + private String join(String... parts) + { + return String.join(" ", parts); + } + + private static String uniqueString() + { + final UUID unique = UUID.randomUUID(); + final StringBuilder result = new StringBuilder(Long.SIZE / 2); + for (long l : List.of(unique.getMostSignificantBits(), unique.getLeastSignificantBits())) + { + for (int i = Long.SIZE - 4; i >= 0; i -= 4) + { + int ch = 'a' + (byte) ((((l & (0xFL << i)) >> i)) & 0xFL); + result.append(Character.toChars(ch)); + } + } + return result.toString(); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchSiteIndexingTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchSiteIndexingTests.java new file mode 100644 index 000000000..8bf8c7f78 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchSiteIndexingTests.java @@ -0,0 +1,522 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.alfresco.elasticsearch.SearchQueryService.req; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +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.Session; +import org.apache.logging.log4j.util.Strings; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.dataprep.AlfrescoHttpClientFactory; +import org.alfresco.dataprep.CMISUtil; +import org.alfresco.dataprep.ContentActions; +import org.alfresco.dataprep.SiteService.Visibility; +import org.alfresco.rest.core.RestWrapper; +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.model.ContentModel; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; +import org.alfresco.utility.report.log.Step; + +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +public class ElasticsearchSiteIndexingTests extends AbstractTestNGSpringContextTests +{ + private static final Iterable LANGUAGES_TO_CHECK = List.of("afts", "lucene"); + private static final FileModel DOCUMENT_LIBRARY = new FileModel("documentLibrary"); + private static final String FILENAME_PREFIX = "EsSiteTest"; + private static final String FILE_CONTENT_CONDITION = " TEXT:" + FILENAME_PREFIX + "*"; + private static final String SAMPLE_SITE_ID = "swsdp"; + private static final String ALL_SITES = "_ALL_SITES_ "; + private static final String EVERYTHING = "_EVERYTHING_ "; + + @Autowired + ServerHealth serverHealth; + + @Autowired + DataUser dataUser; + + @Autowired + DataContent dataContent; + + @Autowired + public DataSite dataSite; + + @Autowired + private AlfrescoHttpClientFactory alfrescoHttpClientFactory; + + @Autowired + SearchQueryService searchQueryService; + + @Autowired + protected RestWrapper restClient; + + private UserModel testUser; + private UserModel siteCreator; + private FolderModel testFolder; + private SiteModel testSite1; + private SiteModel testSite2; + private FileModel fileNotInSite; + private FileModel file1; + private FileModel file2; + private FileModel file3; + private FileModel file4; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + serverHealth.assertServerIsOnline(); + + Step.STEP("Create test users and a folder."); + testUser = dataUser.createRandomTestUser(); + siteCreator = dataUser.createRandomTestUser(); + testFolder = dataContent + .usingAdmin() + .usingResource(contentRoot()) + .createFolder(new FolderModel(unique("FOLDER"))); + } + + @Test(groups = {TestGroup.SEARCH, TestGroup.SITES, TestGroup.REGRESSION}) + public void testSiteUseCasesForCreateModifyDeleteSite() + { + // Remove the automatically created Sample Site + deleteSite(SAMPLE_SITE_ID); + + // Sometimes this test may fail, so if previous data exists it must be deleted before the next run + Stream.of(fileNotInSite, file1, file2, file3, file4) + .filter(Objects::nonNull) + .forEach(this::deleteFile); + Stream.of(testSite1, testSite2) + .filter(Objects::nonNull) + .map(SiteModel::getId) + .forEach(this::deleteSite); + + Step.STEP("Site creation use cases"); + + // Check there are no files within or without a site, that have the given filename prefix + assertSiteQueryResult(ALL_SITES, List.of()); + assertSiteQueryResult(EVERYTHING, "AND", FILE_CONTENT_CONDITION, List.of()); + + // Create a file with the given filename prefix, outside any site + fileNotInSite = new FileModel(unique(FILENAME_PREFIX) + ".txt", FileType.TEXT_PLAIN, "Content for fileNotInSite"); + fileNotInSite = dataContent + .usingAdmin() + .usingResource(testFolder) + .createContent(fileNotInSite); + + // Search with condition of filename prefix - we should see the file using EVERYTHING but not from ALL_SITES + assertSiteQueryResult(ALL_SITES, "AND", FILE_CONTENT_CONDITION, List.of()); + assertSiteQueryResult(EVERYTHING, "AND", FILE_CONTENT_CONDITION, List.of(fileNotInSite)); + + // No sites should exist - expect no results + assertSiteQueryResult(ALL_SITES, List.of()); + assertSiteQueryResult(unique("NoSuchSite"), List.of()); + + // Create one empty public site - expect no results other than document library + testSite1 = createPublicSite(); + assertSiteQueryResult(testSite1.getId(), List.of(DOCUMENT_LIBRARY)); + assertSiteQueryResult(ALL_SITES, List.of(DOCUMENT_LIBRARY)); + assertSiteQueryResult(EVERYTHING, "AND", FILE_CONTENT_CONDITION, List.of(fileNotInSite)); + + // Create a file in public site - expect one file plus the document library. + file1 = createContentInSite(testSite1, FILENAME_PREFIX + "test1"); + assertSiteQueryResult(testSite1.getId(), List.of(DOCUMENT_LIBRARY, file1)); + assertSiteQueryResult(ALL_SITES, List.of(DOCUMENT_LIBRARY, file1)); + assertSiteQueryResult(EVERYTHING, "AND", FILE_CONTENT_CONDITION, List.of(fileNotInSite, file1)); + + // Create another file in public site - expect two files plus the document library. + file2 = createContentInSite(testSite1, FILENAME_PREFIX + "test2"); + assertSiteQueryResult(testSite1.getId(), List.of(DOCUMENT_LIBRARY, file1, file2)); + assertSiteQueryResult(ALL_SITES, List.of(DOCUMENT_LIBRARY, file1, file2)); + assertSiteQueryResult(EVERYTHING, "AND", FILE_CONTENT_CONDITION, List.of(fileNotInSite, file1, file2)); + + // Create a second public site, empty - expect no results other than document library + testSite2 = createPublicSite(); + assertSiteQueryResult(testSite2.getId(), List.of(DOCUMENT_LIBRARY)); + assertSiteQueryResult(testSite1.getId(), List.of(DOCUMENT_LIBRARY, file1, file2)); + assertSiteQueryResult(ALL_SITES, List.of(DOCUMENT_LIBRARY, file1, file2)); + assertSiteQueryResult(EVERYTHING, "AND", FILE_CONTENT_CONDITION, List.of(fileNotInSite, file1, file2)); + + // Create a file in second public site - expect one file plus the document library. + file3 = createContentInSite(testSite2, FILENAME_PREFIX + "test3"); + assertSiteQueryResult(testSite2.getId(), List.of(DOCUMENT_LIBRARY, file3)); + assertSiteQueryResult(testSite1.getId(), List.of(DOCUMENT_LIBRARY, file1, file2)); + assertSiteQueryResult(ALL_SITES, List.of(DOCUMENT_LIBRARY, file1, file2, file3)); + assertSiteQueryResult(EVERYTHING, "AND", FILE_CONTENT_CONDITION, List.of(fileNotInSite, file1, file2, file3)); + + // Create another file in second public site - expect two files plus the document library. + file4 = createContentInSite(testSite2, FILENAME_PREFIX + "test4"); + assertSiteQueryResult(testSite2.getId(), List.of(DOCUMENT_LIBRARY, file3, file4)); + assertSiteQueryResult(testSite1.getId(), List.of(DOCUMENT_LIBRARY, file1, file2)); + assertSiteQueryResult(ALL_SITES, List.of(DOCUMENT_LIBRARY, file1, file2, file3, file4)); + assertSiteQueryResult(EVERYTHING, "AND", FILE_CONTENT_CONDITION, List.of(fileNotInSite, file1, file2, file3, file4)); + + // Test disjunction and conjunction using SITE: and SITE: + assertSiteQueryResult(testSite1.getId(), "OR", " SITE:" + testSite2.getId(), List.of(DOCUMENT_LIBRARY, file1, file2, file3, file4)); + assertSiteQueryResult(testSite1.getId(), "OR", " SITE:" + unique("NoSuchSite"), List.of(DOCUMENT_LIBRARY, file1, file2)); + assertSiteQueryResult(testSite1.getId(), "AND", " SITE:" + unique("NoSuchSite"), List.of()); + + // Test conjunction using SITE: and TEXT: + assertSiteQueryResult(testSite1.getId(), "AND", " TEXT:" + FILENAME_PREFIX + "test2*", List.of(file2)); + assertSiteQueryResult(testSite1.getId(), "AND", " TEXT:" + FILENAME_PREFIX + "testX*", List.of()); + assertSiteQueryResult(EVERYTHING, "AND", " TEXT:" + FILENAME_PREFIX + "test2*", List.of(file2)); + assertSiteQueryResult(EVERYTHING, "AND", " TEXT:" + FILENAME_PREFIX + "testX*", List.of()); + assertSiteQueryResult(ALL_SITES, "AND", " TEXT:" + FILENAME_PREFIX + "test2*", List.of(file2)); + assertSiteQueryResult(ALL_SITES, "AND", " TEXT:" + FILENAME_PREFIX + "testX*", List.of()); + + // Test modify site + Step.STEP("Site modification use cases"); + + // Verify site creating user can see all files in both public sites + assertSiteQueryResult(siteCreator, testSite1.getId(), List.of(DOCUMENT_LIBRARY, file1, file2)); + assertSiteQueryResult(siteCreator, testSite2.getId(), List.of(DOCUMENT_LIBRARY, file3, file4)); + assertSiteQueryResult(siteCreator, ALL_SITES, List.of(DOCUMENT_LIBRARY, file1, file2, file3, file4)); + assertSiteQueryResult(siteCreator, EVERYTHING, "AND", FILE_CONTENT_CONDITION, List.of(fileNotInSite, file1, file2, file3, file4)); + + // Verify user who is not a member of any sites can see all files in both public sites + UserModel publicUser = dataUser.createRandomTestUser(); + assertSiteQueryResult(publicUser, testSite1.getId(), List.of(DOCUMENT_LIBRARY, file1, file2)); + assertSiteQueryResult(publicUser, testSite2.getId(), List.of(DOCUMENT_LIBRARY, file3, file4)); + assertSiteQueryResult(publicUser, ALL_SITES, List.of(DOCUMENT_LIBRARY, file1, file2, file3, file4)); + assertSiteQueryResult(publicUser, EVERYTHING, "AND", FILE_CONTENT_CONDITION, List.of(fileNotInSite, file1, file2, file3, file4)); + + // Change first site's visibility to private - verify site creator can still see all files in both sites + dataSite.usingUser(siteCreator).updateSiteVisibility(testSite1, Visibility.PRIVATE); + assertSiteQueryResult(siteCreator, testSite1.getId(), List.of(DOCUMENT_LIBRARY, file1, file2)); + assertSiteQueryResult(siteCreator, testSite2.getId(), List.of(DOCUMENT_LIBRARY, file3, file4)); + assertSiteQueryResult(siteCreator, ALL_SITES, List.of(DOCUMENT_LIBRARY, file1, file2, file3, file4)); + assertSiteQueryResult(siteCreator, EVERYTHING, "AND", FILE_CONTENT_CONDITION, List.of(fileNotInSite, file1, file2, file3, file4)); + + // Verify user who is not a member of any sites can see files in public site but not see files in private site + assertSiteQueryResult(publicUser, testSite1.getId(), List.of()); + assertSiteQueryResult(publicUser, testSite2.getId(), List.of(DOCUMENT_LIBRARY, file3, file4)); + assertSiteQueryResult(publicUser, ALL_SITES, List.of(DOCUMENT_LIBRARY, file3, file4)); + assertSiteQueryResult(publicUser, EVERYTHING, "AND", FILE_CONTENT_CONDITION, List.of(fileNotInSite, file3, file4)); + + // Change site visibility back to public - user who is not a member of any sites can see files in both sites again + dataSite.usingUser(siteCreator).updateSiteVisibility(testSite1, Visibility.PUBLIC); + assertSiteQueryResult(publicUser, testSite1.getId(), List.of(DOCUMENT_LIBRARY, file1, file2)); + assertSiteQueryResult(publicUser, testSite2.getId(), List.of(DOCUMENT_LIBRARY, file3, file4)); + assertSiteQueryResult(publicUser, ALL_SITES, List.of(DOCUMENT_LIBRARY, file1, file2, file3, file4)); + assertSiteQueryResult(publicUser, EVERYTHING, "AND", FILE_CONTENT_CONDITION, List.of(fileNotInSite, file1, file2, file3, file4)); + + // Test delete site + Step.STEP("Site deletion use cases"); + + // Delete one site - expect no results for that site + deleteSite(testSite1.getId()); + assertSiteQueryResult(testSite1.getId(), List.of()); + assertSiteQueryResult(testSite2.getId(), List.of(DOCUMENT_LIBRARY, file3, file4)); + assertSiteQueryResult(ALL_SITES, List.of(DOCUMENT_LIBRARY, file3, file4)); + assertSiteQueryResult(EVERYTHING, "AND", FILE_CONTENT_CONDITION, List.of(fileNotInSite, file3, file4)); + + // Delete remaining site - expect no results + deleteSite(testSite2.getId()); + assertSiteQueryResult(testSite2.getId(), List.of()); + assertSiteQueryResult(testSite1.getId(), List.of()); + assertSiteQueryResult(ALL_SITES, List.of()); + assertSiteQueryResult(EVERYTHING, "AND", FILE_CONTENT_CONDITION, List.of(fileNotInSite)); + } + + @Test(groups = {TestGroup.SEARCH, TestGroup.SITES, TestGroup.REGRESSION}) + public void manipulatingFilesAndContentBetweenSites() + { + Step.STEP("Moving files between sites and modifying content use cases"); + + SiteModel publicSite1 = createPublicSite(siteCreator); + SiteModel publicSite2 = createPublicSite(siteCreator); + FileModel file5 = createContentInSite(publicSite1, "file5"); + assertSiteQueryResult(publicSite1.getId(), List.of(DOCUMENT_LIBRARY, file5)); + + // Moving a file between sites. + moveFile(file5, publicSite1, publicSite2); + assertSiteQueryResult(publicSite1.getId(), List.of(DOCUMENT_LIBRARY)); + assertSiteQueryResult(publicSite2.getId(), List.of(DOCUMENT_LIBRARY, file5)); + + // Moving a file out of a site. + moveFileOutsideOfSite(file5, publicSite2, testFolder); + assertSiteQueryResult(publicSite2.getId(), List.of(DOCUMENT_LIBRARY)); + + // Moving a file to the site + FileModel file6 = dataContent + .usingAdmin() + .usingResource(contentRoot()) + .createContent(CMISUtil.DocumentType.TEXT_PLAIN); + FileModel file7 = createContentInSite(publicSite1, "file7"); // Ensuring that DocumentLibrary exists in the publicSite1 + assertSiteQueryResult(publicSite1.getId(), List.of(DOCUMENT_LIBRARY, file7)); + moveFileToTheSite(file6, publicSite1); + assertSiteQueryResult(publicSite1.getId(), List.of(DOCUMENT_LIBRARY, file6, file7)); + + // Removing file. + FileModel file8 = createContentInSite(publicSite2, "file8"); + assertSiteQueryResult(publicSite2.getId(), List.of(DOCUMENT_LIBRARY, file8)); + deleteFile(file8); + assertSiteQueryResult(publicSite2.getId(), List.of(DOCUMENT_LIBRARY)); + + // Document modification. + FileModel file9 = createContentInSite(publicSite2, "file9", "Initial Content."); + assertContentInGivenFileUnderSiteQueryResult(publicSite2.getId(), "initial", List.of(file9)); + modifyDocument(file9, "Modified Content."); + assertContentInGivenFileUnderSiteQueryResult(publicSite2.getId(), "modified", List.of(file9)); + + // Cleanup. + deleteFiles(file6, file7, file9); + dataContent.usingAdmin().deleteSite(publicSite1); + dataContent.usingAdmin().deleteSite(publicSite2); + } + + private void deleteFiles(FileModel... fileModels) + { + for (FileModel fileModel : fileModels) + { + deleteFile(fileModel); + } + } + + private void moveFileToTheSite(FileModel file, SiteModel targetSite) + { + Session session = dataContent.getContentActions().getCMISSession(alfrescoHttpClientFactory.getAdminUser(), alfrescoHttpClientFactory.getAdminPassword()); + ContentActions actions = dataContent.usingAdmin().getContentActions(); + CmisObject objFrom = actions.getCmisObject(session, file.getName()); + CmisObject objTarget = session.getObjectByPath("/Sites/" + targetSite.getId() + "/documentLibrary"); + + List parents; + CmisObject parent; + if (objFrom instanceof Document) + { + Document d = (Document) objFrom; + parents = d.getParents(); + parent = session.getObject(((Folder) parents.get(0)).getId()); + d.move(parent, objTarget); + } + else if (objFrom instanceof Folder) + { + Folder f = (Folder) objFrom; + parents = f.getParents(); + parent = session.getObject(((Folder) parents.get(0)).getId()); + f.move(parent, objTarget); + } + } + + private void assertSiteQueryResult(String siteName, Collection contentModels) + { + assertSiteQueryResult(testUser, siteName, contentModels); + } + + private void assertSiteQueryResult(UserModel user, String siteName, Collection contentModels) + { + final List contentNames = contentModels + .stream() + .map(ContentModel::getName) + .collect(Collectors.toList()); + + for (final String language : LANGUAGES_TO_CHECK) + { + Step.STEP("Searching for SITE `" + siteName + "` using `" + language + "` language."); + final SearchRequest query = req(language, "SITE:" + siteName + " "); + if (contentNames.isEmpty()) + { + searchQueryService.expectNoResultsFromQuery(query, user); + } + else + { + Collections.shuffle(contentNames); + searchQueryService.expectResultsFromQuery(query, user, contentNames.toArray(String[]::new)); + } + } + } + + private void assertSiteQueryResult(String site1Name, String operator, String condition, Collection contentModels) + { + assertSiteQueryResult(testUser, site1Name, operator, condition, contentModels); + } + + private void assertSiteQueryResult(UserModel user, String site1Name, String operator, String condition, Collection contentModels) + { + final List contentNames = contentModels + .stream() + .map(ContentModel::getName) + .collect(Collectors.toList()); + + for (final String language : LANGUAGES_TO_CHECK) + { + Step.STEP("Searching for SITE `" + site1Name + "` " + operator + " `" + condition + "` using `" + language + "` language."); + final SearchRequest query = req(language, "SITE:" + site1Name + " " + operator + condition); + if (contentNames.isEmpty()) + { + searchQueryService.expectNoResultsFromQuery(query, user); + } + else + { + Collections.shuffle(contentNames); + searchQueryService.expectResultsFromQuery(query, user, contentNames.toArray(String[]::new)); + } + } + } + + private void assertContentInGivenFileUnderSiteQueryResult(String siteName, String content, Collection contentModels) + { + final List contentNames = contentModels + .stream() + .map(ContentModel::getName) + .collect(Collectors.toList()); + for (final String language : LANGUAGES_TO_CHECK) + { + Step.STEP("Searching for SITE `" + siteName + "` and content:'" + content + "' using `" + language + "` language."); + final SearchRequest query = req(language, "SITE:" + siteName + " AND TEXT:" + content + " "); + searchQueryService.expectResultsFromQuery(query, testUser, contentNames.toArray(String[]::new)); + } + } + + private void modifyDocument(FileModel file, String newContent) + { + dataContent.usingUser(siteCreator).usingResource(file) + .updateContent(newContent); + } + + private void deleteFile(ContentModel contentModel) + { + dataContent + .usingAdmin() + .usingResource(contentModel) + .deleteContent(); + } + + private void moveFile(FileModel file, SiteModel sourceSite, SiteModel targetSite) + { + moveFile(file, sourceSite, targetSite, null); + } + + private void moveFile(FileModel file, SiteModel sourceSite, SiteModel targetSite, ContentModel targetFolder) + { + Session session = dataContent.getContentActions().getCMISSession(siteCreator.getUsername(), siteCreator.getPassword()); + dataContent.usingUser(siteCreator).getContentActions() + .moveTo( + session, + sourceSite.getId(), + file.getName(), + targetSite.getId(), + targetFolder != null ? targetFolder.getName() : Strings.EMPTY // if empty then target folder is documentLibrary (which is default target location) + ); + } + + private void moveFileOutsideOfSite(FileModel file, SiteModel sourceSite, ContentModel targetContentModel) + { + Session session = dataContent.getContentActions().getCMISSession(alfrescoHttpClientFactory.getAdminUser(), alfrescoHttpClientFactory.getAdminPassword()); + ContentActions actions = dataContent.usingAdmin().getContentActions(); + CmisObject objFrom = actions.getCmisObject(session, sourceSite.getId(), file.getName()); + CmisObject objTarget = actions.getCmisObject(session, targetContentModel.getCmisLocation()); + + List parents; + CmisObject parent; + if (objFrom instanceof Document) + { + Document d = (Document) objFrom; + parents = d.getParents(); + parent = session.getObject(((Folder) parents.get(0)).getId()); + d.move(parent, objTarget); + } + else if (objFrom instanceof Folder) + { + Folder f = (Folder) objFrom; + parents = f.getParents(); + parent = session.getObject(((Folder) parents.get(0)).getId()); + f.move(parent, objTarget); + } + } + + private FileModel createContentInSite(SiteModel site, String fileName) + { + return createContentInSite(site, fileName, "Content for " + fileName); + } + + private FileModel createContentInSite(SiteModel site, String fileName, String content) + { + final FileModel file = new FileModel(unique(fileName) + ".txt", FileType.TEXT_PLAIN, content); + return dataContent.usingUser(siteCreator) + .usingSite(site) + .createContent(file); + } + + private SiteModel createPublicSite() + { + return createPublicSite(siteCreator); + } + + private SiteModel createPublicSite(UserModel user) + { + SiteModel createdSite = dataSite.usingUser(user).createPublicRandomSite(); + Step.STEP("Created public site '" + createdSite.getId() + "'."); + return createdSite; + } + + private void deleteSite(String siteId) + { + Step.STEP("Deleting site '" + siteId + "', if it exists"); + SiteModel siteToDelete = new SiteModel(siteId); + if (dataSite.usingAdmin().isSiteCreated(siteToDelete)) + { + dataSite.usingAdmin().deleteSite(siteToDelete); + } + } + + private static ContentModel contentRoot() + { + final ContentModel root = new ContentModel("-root-"); + root.setNodeRef(root.getName()); + return root; + } + + private static String unique(String prefix) + { + return prefix + "-" + UUID.randomUUID(); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchTagIndexingTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchTagIndexingTests.java new file mode 100644 index 000000000..c10bd9ea0 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchTagIndexingTests.java @@ -0,0 +1,279 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.springframework.http.HttpStatus.OK; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import static org.alfresco.elasticsearch.SearchQueryService.req; + +import java.util.*; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.dataprep.AlfrescoHttpClientFactory; +import org.alfresco.rest.core.RestWrapper; +import org.alfresco.rest.model.RestTagModel; +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.model.*; +import org.alfresco.utility.network.ServerHealth; +import org.alfresco.utility.report.log.Step; + +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +public class ElasticsearchTagIndexingTests extends AbstractTestNGSpringContextTests +{ + private static final Iterable LANGUAGES_TO_CHECK = List.of("afts", "lucene"); + + @Autowired + ServerHealth serverHealth; + + @Autowired + DataUser dataUser; + + @Autowired + DataContent dataContent; + + @Autowired + private AlfrescoHttpClientFactory alfrescoHttpClientFactory; + + @Autowired + SearchQueryService searchQueryService; + + @Autowired + protected RestWrapper restClient; + + private UserModel testUser; + private FolderModel testFolder; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + serverHealth.assertServerIsOnline(); + + testUser = dataUser.createRandomTestUser(); + testFolder = dataContent + .usingAdmin() + .usingResource(contentRoot()) + .createFolder(new FolderModel(unique("FOLDER"))); + } + + @Test(groups = {TestGroup.SEARCH, TestGroup.TAGS, TestGroup.REGRESSION}) + public void testTAGUseCases() + { + final String tag1 = unique("TAG1"); + + // No result for not existing tag + assertTagQueryResult(tag1, List.of()); + + // Tag first file - tag is created - expect single result + final FileModel file1 = givenFile("test1"); + tagContent(file1, tag1); + assertTagQueryResult(tag1, List.of(file1)); + + // Tag another file - tag is reused - expect two items in the result + final FileModel file2 = givenFile("test2"); + tagContent(file2, tag1); + assertTagQueryResult(tag1, List.of(file1, file2)); + + final String tag2 = unique("TAG2"); + + // Just a sanity check - No result for the second tag + assertTagQueryResult(tag2, List.of()); + + // Tag second file with a new tag + final String tag2Id = tagContent(file2, tag2); + assertTagQueryResult(tag1, List.of(file1, file2)); + assertTagQueryResult(tag2, List.of(file2)); + + // Tag new file with the second tag + final FileModel file3 = givenFile("test3"); + tagContent(file3, tag2); + assertTagQueryResult(tag1, List.of(file1, file2)); + assertTagQueryResult(tag2, List.of(file2, file3)); + + // test disjunction and conjunction + assertTagQueryResult(tag1, "OR", tag2, List.of(file1, file2, file3)); + assertTagQueryResult(tag1, "AND", tag2, List.of(file2)); + final String unknownTag = unique("unknown"); + assertTagQueryResult(tag1, "OR", unknownTag, List.of(file1, file2)); + assertTagQueryResult(tag1, "AND", unknownTag, List.of()); + + // Delete file + deleteFile(file1); + assertTagQueryResult(tag1, List.of(file2)); + assertTagQueryResult(tag2, List.of(file2, file3)); + + // Rename tag + final String newTag2 = unique("NEW-TAG2"); + assertTagQueryResult(newTag2, List.of()); + renameTag(tag2Id, newTag2); + assertTagQueryResult(tag1, List.of(file2)); + assertTagQueryResult(newTag2, List.of(file2, file3)); + assertTagQueryResult(tag2, List.of()); + + // Delete tag + deleteTag(file3, newTag2); + assertTagQueryResult(tag1, List.of(file2)); + assertTagQueryResult(tag2, List.of()); + assertTagQueryResult(newTag2, List.of(file2)); + } + + private void assertTagQueryResult(String tag1Name, String operator, String tag2Name, Collection contentModels) + { + final List contentNames = contentModels + .stream() + .map(ContentModel::getName) + .collect(Collectors.toList()); + + for (final String tag1 : tagVariantsToCheck(tag1Name)) + { + for (final String tag2 : tagVariantsToCheck(tag2Name)) + { + for (final String language : LANGUAGES_TO_CHECK) + { + Step.STEP("Searching for TAG `" + tag1 + "` " + operator + " `" + tag2 + "` using `" + language + "` language."); + final SearchRequest query = req(language, "TAG:" + tag1 + " " + operator + " TAG:" + tag2); + if (contentNames.isEmpty()) + { + searchQueryService.expectNoResultsFromQuery(query, testUser); + } + else + { + Collections.shuffle(contentNames); + searchQueryService.expectResultsFromQuery(query, testUser, contentNames.toArray(String[]::new)); + } + } + } + } + } + + private void assertTagQueryResult(String tagName, Collection contentModels) + { + final List contentNames = contentModels + .stream() + .map(ContentModel::getName) + .collect(Collectors.toList()); + + for (final String tag : tagVariantsToCheck(tagName)) + { + for (final String language : LANGUAGES_TO_CHECK) + { + Step.STEP("Searching for TAG `" + tag + "` using `" + language + "` language."); + final SearchRequest query = req(language, "TAG:" + tag); + if (contentNames.isEmpty()) + { + searchQueryService.expectNoResultsFromQuery(query, testUser); + } + else + { + Collections.shuffle(contentNames); + searchQueryService.expectResultsFromQuery(query, testUser, contentNames.toArray(String[]::new)); + } + } + } + } + + private Iterable tagVariantsToCheck(String tagName) + { + return List.of( + tagName.toLowerCase(Locale.ROOT), + tagName.toUpperCase(Locale.ROOT), + "\"" + tagName.toLowerCase(Locale.ROOT) + "\"", + "\"" + tagName.toUpperCase(Locale.ROOT) + "\""); + } + + private FileModel givenFile(final String fileName) + { + final FileModel file = new FileModel(unique(fileName), FileType.TEXT_PLAIN, "Content for " + fileName); + return dataContent + .usingAdmin() + .usingResource(testFolder) + .createContent(file); + } + + private String tagContent(ContentModel contentModel, String tag) + { + var tagModel = new TagModel(tag); + dataContent + .usingAdmin() + .usingResource(contentModel) + .addTagToContent(tagModel); + + final String tagNodeId = dataContent.getContentActions().getTagNodeRef( + dataContent.getCurrentUser().getUsername(), dataContent.getCurrentUser().getPassword(), + contentModel.getCmisLocation(), tag); + assertNotNull(tagNodeId, "Tag node ref must exist."); + return tagNodeId; + } + + private void deleteFile(ContentModel contentModel) + { + dataContent + .usingAdmin() + .usingResource(contentModel) + .deleteContent(); + } + + private void deleteTag(ContentModel contentModel, String tagName) + { + boolean deleted = dataContent + .usingAdmin() + .getContentActions() + .removeTag( + dataContent.getCurrentUser().getUsername(), dataContent.getCurrentUser().getPassword(), + contentModel.getCmisLocation(), tagName); + assertTrue(deleted, "Tag should be deleted."); + } + + private void renameTag(String tagId, String newName) + { + RestTagModel tag = RestTagModel.builder().id(tagId).create(); + RestTagModel update = restClient.authenticateUser(dataContent.getAdminUser()).withCoreAPI().usingTag(tag).update(newName); + restClient.assertStatusCodeIs(OK); + } + + private static ContentModel contentRoot() + { + final ContentModel root = new ContentModel("-root-"); + root.setNodeRef(root.getName()); + return root; + } + + private static String unique(String prefix) + { + return prefix + "-" + UUID.randomUUID(); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchTemplateSearchTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchTemplateSearchTests.java new file mode 100644 index 000000000..8ccdd0c76 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchTemplateSearchTests.java @@ -0,0 +1,316 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.alfresco.elasticsearch.SearchQueryService.req; +import static org.alfresco.utility.data.RandomData.getRandomFile; +import static org.alfresco.utility.data.RandomData.getRandomName; +import static org.alfresco.utility.report.log.Step.STEP; + +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.model.RestTagModel; +import org.alfresco.rest.search.RestRequestDefaultsModel; +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.Utility; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.model.ContentModel; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; + +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +public class ElasticsearchTemplateSearchTests extends AbstractTestNGSpringContextTests +{ + private static final String SEARCH_TERM = "sample"; + + @Autowired + private ServerHealth serverHealth; + + @Autowired + private DataUser dataUser; + + @Autowired + private DataContent dataContent; + + @Autowired + private SearchQueryService searchQueryService; + + private UserModel testUser; + private FolderModel testParentFolder; + /** Pre-built ANCESTOR clause for the test parent folder. */ + private String ancestorClause; + private ContentModel fileWithTermInName; + private ContentModel fileWithPhraseInContent; + private ContentModel fileWithTermInTitle; + private ContentModel fileWithTermInDescription; + private ContentModel fileWithTermInTag; + private ContentModel fileWithDifferentTermInName; + private ContentModel folderWithTermInName; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + serverHealth.assertServerIsOnline(); + + STEP("Create a dedicated parent folder so all queries can be scoped via ANCESTOR"); + testParentFolder = createTestParentFolder(); + ancestorClause = "ANCESTOR:\"" + testParentFolder.getNodeRef() + "\""; + + STEP("Create a test user and few files and folders containing searched term in name, title, description, content and tag"); + fileWithTermInName = createFile(SEARCH_TERM + ".txt", "some text"); + fileWithPhraseInContent = createFile(getRandomFile(FileType.TEXT_PLAIN), "Dummy " + SEARCH_TERM + " irrelevant text"); + fileWithTermInTitle = createRandomFileWithTitle(SEARCH_TERM); + fileWithTermInDescription = createRandomFileWithDescription(SEARCH_TERM); + fileWithTermInTag = createRandomFileWithTag(SEARCH_TERM); + fileWithDifferentTermInName = createFile("dummy.txt", "content without phrase"); + folderWithTermInName = createFolder(SEARCH_TERM); + + testUser = dataUser.createRandomTestUser(); + + STEP("Wait for the batch indexer to index the last-created node under the test parent folder"); + SearchRequest probe = req("afts", ancestorClause + " AND TYPE:'cm:folder'", Map.of()); + Utility.sleep(500, 60000, () -> searchQueryService.expectNodeRefsFromQuery( + probe, dataUser.getAdminUser(), folderWithTermInName.getNodeRef())); + } + + @AfterClass + public void dataCleanup() + { + STEP("Clean up the test parent folder (cascades to all children) and the test user"); + if (testParentFolder != null) + { + dataContent.usingAdmin().usingResource(testParentFolder).deleteContent(); + } + if (testUser != null) + { + dataUser.deleteUser(testUser); + } + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_simpleTemplate() + { + STEP("Search for files by name using simple template with one property"); + Map templates = Map.of("_NODE", "%cm:name"); + String query = ancestorClause + " AND TYPE:'cm:content' AND _NODE:" + SEARCH_TERM; + SearchRequest request = req("afts", query, templates); + + searchQueryService.expectNodeRefsFromQuery(request, testUser, fileWithTermInName.getNodeRef()); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_simpleTemplateWithPhrase() + { + STEP("Search for files containing specific phrase using simple template with one property"); + Map templates = Map.of("_NODE", "%TEXT"); + String query = ancestorClause + " AND TYPE:'cm:content' AND _NODE:\"" + SEARCH_TERM + " irrelevant\""; + SearchRequest request = req("afts", query, templates); + + searchQueryService.expectNodeRefsFromQuery(request, testUser, fileWithPhraseInContent.getNodeRef()); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_templateWithTwoParameters() + { + STEP("Search for files using template containing multiple properties"); + Map templates = Map.of("_NODE", "%(cm:name cm:title)"); + String query = ancestorClause + " AND TYPE:'cm:content' AND _NODE:" + SEARCH_TERM; + SearchRequest request = req("afts", query, templates); + + searchQueryService.expectNodeRefsFromQuery(request, testUser, fileWithTermInName.getNodeRef(), fileWithTermInTitle.getNodeRef()); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_nestedTemplate() + { + Map templates = Map.of( + "_NODE", "%(cm:name cm:title)", + "_NODET", "%(_NODEX TAG)", + "_NODEX", "%(_NODE cm:description)" + + ); + STEP("Search for files using more complex two-level nested template containing multiple properties"); + String query = ancestorClause + " AND TYPE:'cm:content' AND _NODEX:" + SEARCH_TERM; + SearchRequest request = req("afts", query, templates); + searchQueryService.expectNodeRefsFromQuery(request, testUser, + fileWithTermInName.getNodeRef(), fileWithTermInDescription.getNodeRef(), fileWithTermInTitle.getNodeRef()); + + STEP("Search for files using three-level nested template containing multiple properties"); + String queryIncludingTag = ancestorClause + " AND TYPE:'cm:content' AND _NODET:" + SEARCH_TERM; + SearchRequest requestIncludingTag = req("afts", queryIncludingTag, templates); + searchQueryService.expectNodeRefsFromQuery(requestIncludingTag, testUser, + fileWithTermInName.getNodeRef(), fileWithTermInDescription.getNodeRef(), fileWithTermInTitle.getNodeRef(), fileWithTermInTag.getNodeRef()); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_templateNameAsQueryDefaultFieldName() + { + STEP("Search for files using template containing multiple properties, as a default search field"); + Map templates = Map.of("_NODE", "%(cm:name cm:title)"); + // ANCESTOR must be its own clause; only the bare term uses the default field name. + String query = ancestorClause + " AND TYPE:'cm:content' AND " + SEARCH_TERM; + SearchRequest request = req("afts", query, templates); + request.setDefaults(RestRequestDefaultsModel.builder().defaultFieldName("_NODE").create()); + + searchQueryService.expectNodeRefsFromQuery(request, testUser, fileWithTermInName.getNodeRef(), fileWithTermInTitle.getNodeRef()); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_templateWithFixedValue() + { + STEP("Search for files and folders using template containing multiple properties, including a fixed one"); + Map templates = Map.of("_NODE", "%cm:name AND TYPE:'cm:folder'"); + String query = ancestorClause + " AND _NODE:" + SEARCH_TERM; + SearchRequest request = req("afts", query, templates); + + searchQueryService.expectNodeRefsFromQuery(request, testUser, folderWithTermInName.getNodeRef()); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_boostedTemplate() + { + STEP("Search for files using templates and boosts, where second term has higher priority "); + Map templates = Map.of("_NODE", "%cm:name"); + String query = ancestorClause + " AND TYPE:'cm:content' AND (_NODE:" + SEARCH_TERM + "^0.5 OR _NODE:dummy^2)"; + SearchRequest request = req("afts", query, templates); + searchQueryService.expectResultsInOrder(request, testUser, fileWithDifferentTermInName.getName(), fileWithTermInName.getName()); + + STEP("Search for files using templates and boosts, where first term has higher priority "); + String queryInvertedBoost = ancestorClause + " AND TYPE:'cm:content' AND (_NODE:" + SEARCH_TERM + "^2 OR _NODE:dummy^0.5)"; + SearchRequest requestInvertedBoost = req("afts", queryInvertedBoost, templates); + searchQueryService.expectResultsInOrder(requestInvertedBoost, testUser, fileWithTermInName.getName(), fileWithDifferentTermInName.getName()); + } + + @Test(groups = {TestGroup.SEARCH}) + public void testAftsQuery_expandedTemplate() + { + Map templates = Map.of("_NODE", "%cm:name"); + String query = ancestorClause + " AND TYPE:'cm:content' AND ~_NODE:" + SEARCH_TERM; + SearchRequest request = req("afts", query, templates); + + searchQueryService.expectNodeRefsFromQuery(request, testUser, fileWithTermInName.getNodeRef()); + } + + private FolderModel createTestParentFolder() + { + ContentModel contentRoot = new ContentModel("-root-"); + contentRoot.setNodeRef(contentRoot.getName()); + FolderModel parent = new FolderModel(getRandomName("templateSearchTests-")); + return dataContent + .usingAdmin() + .usingResource(contentRoot) + .createFolder(parent); + } + + private ContentModel createRandomFileWithTitle(String title) + { + return createRandomFile(title, null, null); + } + + private ContentModel createRandomFileWithDescription(String description) + { + return createRandomFile(null, description, null); + } + + private ContentModel createRandomFileWithTag(String tag) + { + return createRandomFile(null, null, tag); + } + + private ContentModel createRandomFile(String title, String description, String tag) + { + return createFile(getRandomFile(FileType.TEXT_PLAIN), getRandomName("dummy text "), title, description, tag); + } + + private ContentModel createFile(String filename, String content) + { + return createFile(filename, content, null, null, null); + } + + private ContentModel createFile(String filename, String content, String title, String description, String tag) + { + FileModel fileModel = new FileModel(filename, FileType.TEXT_PLAIN, content); + fileModel.setTitle(title); + fileModel.setDescription(description); + + // Create inside the dedicated parent folder so queries scoped with ANCESTOR will pick this up + // (and so bootstrap / cross-class content does NOT match those queries). + FileModel file = dataContent + .usingAdmin() + .usingResource(testParentFolder) + .createContent(fileModel); + + if (StringUtils.isNotBlank(tag)) + { + dataContent + .usingAdmin() + .usingResource(file) + .addTagToContent(RestTagModel.builder().tag(tag).create()); + } + + return file; + } + + private ContentModel createFolder(String folderName) + { + return createFolder(folderName, null, null, null); + } + + private ContentModel createFolder(String folderName, String title, String description, String tag) + { + FolderModel folderModel = new FolderModel(folderName, title, description); + + FolderModel folder = dataContent + .usingAdmin() + .usingResource(testParentFolder) + .createFolder(folderModel); + + if (StringUtils.isNotBlank(tag)) + { + dataContent + .usingAdmin() + .usingResource(folder) + .addTagToContent(RestTagModel.builder().tag(tag).create()); + } + + return folder; + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchTokenisationTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchTokenisationTests.java new file mode 100644 index 000000000..6b4f50160 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/ElasticsearchTokenisationTests.java @@ -0,0 +1,198 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.alfresco.elasticsearch.SearchQueryService.req; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; + +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +/** + * Tests verifying a range of simple AFTS queries using the cm:name field. + */ +public class ElasticsearchTokenisationTests extends AbstractTestNGSpringContextTests +{ + private static final String ALPHABETIC_NO_SPACE = "TestFileabc.txt"; + private static final String NUMERIC_NO_SPACE = "TestFile123.txt"; + private static final String ALPHABETIC_WITH_SPACE = "TestFile abc.txt"; + private static final String NUMERIC_WITH_SPACE = "TestFile 123.txt"; + + @Autowired + private DataUser dataUser; + + @Autowired + private DataContent dataContent; + + @Autowired + private DataSite dataSite; + + @Autowired + private ServerHealth serverHealth; + + @Autowired + protected SearchQueryService searchQueryService; + + private UserModel user; + private SiteModel siteModel; + + /** + * Create a site containing four documents. + */ + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + serverHealth.assertServerIsOnline(); + + user = dataUser.createRandomTestUser(); + siteModel = dataSite.usingUser(user).createPrivateRandomSite(); + + createContent(ALPHABETIC_NO_SPACE, "", siteModel, user); + createContent(NUMERIC_NO_SPACE, "", siteModel, user); + createContent(ALPHABETIC_WITH_SPACE, "", siteModel, user); + createContent(NUMERIC_WITH_SPACE, "", siteModel, user); + } + + /** Check that searching for "TestFileabc.txt" only returns that file. */ + @Test(groups = TestGroup.SEARCH) + public void searchNoSpaceAlphabetic() + { + searchQueryService.expectResultsFromQuery(req("name:\"TestFileabc.txt\""), user, ALPHABETIC_NO_SPACE); + } + + /** Check that searching for "TestFile abc.txt" only returns that file. */ + @Test(groups = TestGroup.SEARCH) + public void searchWithSpaceAlphabetic() + { + searchQueryService.expectResultsFromQuery(req("name:\"TestFile abc.txt\""), user, ALPHABETIC_WITH_SPACE); + } + + /** Check that searching for "TestFile123.txt" returns both files with numbers in. */ + @Test(groups = TestGroup.SEARCH) + public void searchNoSpaceNumeric() + { + searchQueryService.expectResultsFromQuery(req("name:\"TestFile123.txt\""), user, NUMERIC_NO_SPACE, NUMERIC_WITH_SPACE); + } + + /** Check that searching for "TestFile 123.txt" returns both files with numbers in. */ + @Test(groups = TestGroup.SEARCH) + public void searchWithSpaceNumeric() + { + searchQueryService.expectResultsFromQuery(req("name:\"TestFile 123.txt\""), user, NUMERIC_NO_SPACE, NUMERIC_WITH_SPACE); + } + + /** Check that searching for "TestFile" doesn't return TestFileabc.txt. */ + @Test(groups = TestGroup.SEARCH) + public void searchBaseFileName() + { + searchQueryService.expectResultsFromQuery(req("name:\"TestFile\""), user, NUMERIC_NO_SPACE, ALPHABETIC_WITH_SPACE, NUMERIC_WITH_SPACE); + } + + /** Check that searching for TestFileabc.txt with no quotes returns just that file. */ + @Test(groups = TestGroup.SEARCH) + public void searchNoQuotesNoSpaceAlphabetic() + { + searchQueryService.expectResultsFromQuery(req("name:TestFileabc.txt"), user, ALPHABETIC_NO_SPACE); + } + + /** Check that searching for TestFile123.txt with no quotes returns both files with numbers in. */ + @Test(groups = TestGroup.SEARCH) + public void searchNoQuotesNoSpaceNumeric() + { + searchQueryService.expectResultsFromQuery(req("name:TestFile123.txt"), user, NUMERIC_NO_SPACE, NUMERIC_WITH_SPACE); + } + + /** Check that searching for TestFile doesn't return TestFileabc.txt. */ + @Test(groups = TestGroup.SEARCH) + public void searchBaseFileNameNoQuotes() + { + searchQueryService.expectResultsFromQuery(req("name:TestFile"), user, NUMERIC_NO_SPACE, ALPHABETIC_WITH_SPACE, NUMERIC_WITH_SPACE); + } + + /** Check that including an escaped space (_x0020_) in double quotes means that nothing is matched. */ + @Test(groups = TestGroup.SEARCH) + public void searchEncodedSpaceInQuotes() + { + searchQueryService.expectNoResultsFromQuery(req("name:\"TestFile_x0020_abc.txt\""), user); + } + + /** Check that searching for 'TestFileabc.txt' only returns that file. */ + @Test(groups = TestGroup.SEARCH) + public void searchNoSpaceAlphabeticSingleQuote() + { + searchQueryService.expectResultsFromQuery(req("name:'TestFileabc.txt'"), user, ALPHABETIC_NO_SPACE); + } + + /** Check that searching for 'TestFile abc.txt' only returns that file. */ + @Test(groups = TestGroup.SEARCH) + public void searchWithSpaceAlphabeticSingleQuote() + { + searchQueryService.expectResultsFromQuery(req("name:'TestFile abc.txt'"), user, ALPHABETIC_WITH_SPACE); + } + + /** Check that searching for 'TestFile123.txt' returns both files with numbers in. */ + @Test(groups = TestGroup.SEARCH) + public void searchNoSpaceNumericSingleQuote() + { + searchQueryService.expectResultsFromQuery(req("name:'TestFile123.txt'"), user, NUMERIC_NO_SPACE, NUMERIC_WITH_SPACE); + } + + /** Check that searching for 'TestFile 123.txt' returns both files with numbers in. */ + @Test(groups = TestGroup.SEARCH) + public void searchWithSpaceNumericSingleQuote() + { + searchQueryService.expectResultsFromQuery(req("name:'TestFile 123.txt'"), user, NUMERIC_NO_SPACE, NUMERIC_WITH_SPACE); + } + + /** Check that searching for 'TestFile' doesn't return TestFileabc.txt. */ + @Test(groups = TestGroup.SEARCH) + public void searchBaseFileNameSingleQuote() + { + searchQueryService.expectResultsFromQuery(req("name:'TestFile'"), user, NUMERIC_NO_SPACE, ALPHABETIC_WITH_SPACE, NUMERIC_WITH_SPACE); + } + + private FileModel createContent(String filename, String content, SiteModel site, UserModel user) + { + FileModel fileModel = new FileModel(filename, FileType.TEXT_PLAIN, content); + return dataContent.usingUser(user).usingSite(site) + .createContent(fileModel); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodeWithCategoryIndexingTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodeWithCategoryIndexingTests.java new file mode 100644 index 000000000..2e09a642a --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodeWithCategoryIndexingTests.java @@ -0,0 +1,192 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static java.lang.String.format; + +import static org.alfresco.elasticsearch.SearchQueryService.req; +import static org.alfresco.utility.report.log.Step.STEP; + +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.repo.resource.Categories; +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.utility.model.TestGroup; + +/** + * Tests verifying indexing of secondary children and ANCESTOR and CATEGORY index in Elasticsearch. + */ +@SuppressWarnings({"PMD.JUnitTestsShouldIncludeAssert", "PMD.MethodNamingConventions", "PMD.LocalVariableNamingConventions"}) // these are testng tests and use searchQueryService.expectResultsFromQuery for assertion +public class NodeWithCategoryIndexingTests extends NodesSecondaryChildrenRelatedTests +{ + + @Autowired + private Categories categories; + + /* A --- B (folders) \____ \ K --- L (categories) */ + @BeforeClass(alwaysRun = true) + @Override + public void dataPreparation() + { + super.dataPreparation(); + + // given + STEP("Create nested folders in site's Document Library."); + folders.add().nestedRandomFolders(A, B).create(); + + STEP("Create nested categories."); + categories.add().nestedRandomCategories(K, L).create(); + + STEP("Link folders to category."); + folders.modify(A).linkTo(categories.get(L)); + } + + @Test(groups = TestGroup.SEARCH) + public void testParentQueryAgainstCategory() + { + // then + STEP("Verify that searching by PARENT and category will find one descendant node: categoryL."); + SearchRequest query = req("PARENT:" + categories.get(K).getId()); + searchQueryService.expectResultsFromQuery(query, testUser, categories.get(L).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testParentQueryAgainstFolder() + { + // then + STEP("Verify that searching by PARENT and category will find one descendant node: folderA."); + SearchRequest query = req("PARENT:" + categories.get(L).getId()); + searchQueryService.expectResultsFromQuery(query, testUser, folders.get(A).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testParentQueryAgainstFolderAfterCategoryDeletion() + { + // given + STEP("Create nested folders in site's Document Library."); + folders.add().randomFolder(C).create(); + + STEP("Create nested categories."); + categories.add().randomCategory(M).create(); + + STEP("Link folders to category."); + folders.modify(C).linkTo(categories.get(M)); + + // when + STEP("Verify that searching by PARENT and category will find one descendant node: folderC."); + SearchRequest query = req("PARENT:" + categories.get(M).getId()); + searchQueryService.expectResultsFromQuery(query, testUser, folders.get(C).getName()); + + // then + STEP("Delete categoryM."); + categories.delete(categories.get(M)); + + STEP("Verify that searching by PARENT and deleted category will find no descendant nodes."); + searchQueryService.expectResultsFromQuery(query, testUser); + } + + @Test(groups = TestGroup.SEARCH, enabled = false) // re-enable after ACS-6588, ACS-6592 + public void testParentQueryAgainstFolderAfterParentCategoryDeletion() + { + // given + STEP("Create nested folders in site's Document Library."); + folders.add().randomFolder(X).create(); + + STEP("Create nested categories."); + categories.add().nestedRandomCategories(P, Q).create(); + + STEP("Link folders to category."); + folders.modify(X).linkTo(categories.get(Q)); + + // when + STEP("Verify that searching by PARENT and category will find one descendant node: folderC."); + SearchRequest query = req("PARENT:" + categories.get(Q).getId()); + searchQueryService.expectResultsFromQuery(query, testUser, folders.get(X).getName()); + + // then + STEP("Delete categoryP."); + categories.delete(categories.get(P)); + + STEP("Verify that searching by PARENT and deleted category will find no descendant nodes."); + searchQueryService.expectResultsFromQuery(query, testUser); + } + + @Test(groups = TestGroup.SEARCH) + public void testSearchByPath() + { + // given + String Kname = categories.get(K).getName(); + String Lname = categories.get(L).getName(); + String Aname = folders.get(A).getName(); + + // then + STEP("Verify that searching by PATH and category will find: folderA"); + SearchRequest query = req(format("PATH:'/cm:categoryRoot/cm:generalclassifiable/cm:%s/cm:%s/cm:%s'", Kname, Lname, Aname)); + searchQueryService.expectResultsFromQuery(query, testUser, folders.get(A).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void ensureCategoriesAreNotTransitive_lookingForExactChildren() + { + // given + String Kname = categories.get(K).getName(); + String Lname = categories.get(L).getName(); + String Aname = folders.get(A).getName(); + String Bname = folders.get(B).getName(); + + // then + STEP("Verify that searching by PATH for nested folder will return no results (Dependency to category is not transitive)"); + SearchRequest query = req(format("PATH:'/cm:categoryRoot/cm:generalclassifiable/cm:%s/cm:%s/cm:%s/cm:%s'", Kname, Lname, Aname, Bname)); + searchQueryService.expectNoResultsFromQuery(query, testUser); + } + + @Test(groups = TestGroup.SEARCH) + public void ensureCategoriesAreNotTransitive_lookingForAllDescendants() + { + // given + String Kname = categories.get(K).getName(); + String Lname = categories.get(L).getName(); + + // then + STEP("Verify that searching by PATH and category will find: folderA"); + SearchRequest query = req(format("PATH:'/cm:categoryRoot/cm:generalclassifiable/cm:%s/cm:%s//*'", Kname, Lname)); + searchQueryService.expectResultsFromQuery(query, testUser, folders.get(A).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void ensureCategoriesAreNotTransitive_lookingForAllDescendants_byParentCategory() + { + // given + String Kname = categories.get(K).getName(); + + // then + STEP("Verify that searching by PATH and category will find: folderA"); + SearchRequest query = req(format("PATH:'/cm:categoryRoot/cm:generalclassifiable/cm:%s//*'", Kname)); + searchQueryService.expectResultsFromQuery(query, testUser, categories.get(L).getName(), folders.get(A).getName()); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodesSecondaryAncestorIndexingTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodesSecondaryAncestorIndexingTests.java new file mode 100644 index 000000000..24649b72d --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodesSecondaryAncestorIndexingTests.java @@ -0,0 +1,368 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.alfresco.elasticsearch.SearchQueryService.req; +import static org.alfresco.utility.report.log.Step.STEP; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.utility.Utility; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.TestGroup; + +/** + * Tests verifying indexing of secondary children and ANCESTOR index in Elasticsearch. + */ +@SuppressWarnings({"PMD.JUnitTestsShouldIncludeAssert"}) // these are TAS E2E tests and use searchQueryService.expectResultsFromQuery for assertion +public class NodesSecondaryAncestorIndexingTests extends NodesSecondaryChildrenRelatedTests +{ + + private FileModel fileInP; + + /** + * Creates a user and a private site containing below hierarchy of folders. + * + *

+     * Site
+     * DL (Document Library)
+     *  += fA += fB += fC (folderC)
+     *         /     / |
+     *        +     +  +
+     *  += fK += fL += fM
+     *     |     +
+     *     +     |
+     *  += fX += fY += fZ
+     *  += fP += file -+ fA
+     *  += fQ
+     *  += fR
+     *  += fS
+     * 
+ * + * Parent += Child - primary parent-child relationship Parent +- Child - secondary parent-child relationship + */ + @BeforeClass(alwaysRun = true) + @Override + public void dataPreparation() + { + super.dataPreparation(); + + // given + STEP("Create few sets of nested folders in site's Document Library."); + folders.add().nestedRandomFolders(A, B, C).create(); + folders.add().nestedRandomFolders(K, L, M).create(); + folders.add().nestedRandomFolders(X, Y, Z).create(); + folders.add().randomFolders(P, Q, R, S).create(); + fileInP = folders.modify(P).add().randomFile().create(); + + STEP("Create few secondary parent-child relationships."); + folders.modify(K).add().secondaryContent(folders.get(B)); + folders.modify(X).add().secondaryContent(folders.get(K)); + folders.modify(L).add().secondaryContent(folders.get(C), folders.get(Y)); + folders.modify(M).add().secondaryContent(folders.get(C)); + folders.modify(A).add().secondaryContent(fileInP); + } + + @Test(groups = TestGroup.SEARCH) + public void testSecondaryAncestorWithNodeHavingOneSecondaryChild() + { + // then + STEP("Verify that searching by ANCESTOR and folderM will find one descendant node: folderC."); + SearchRequest query = req("ANCESTOR:" + folders.get(M).getNodeRef()); + searchQueryService.expectResultsFromQuery(query, testUser, + folders.get(C).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testSecondaryAncestorWithNodeHavingTwoSecondaryChildren() + { + // then + STEP("Verify that searching by ANCESTOR and folderL will find nodes: folderM, folderC, folderY and folderZ."); + SearchRequest queryAncestorL = req("ANCESTOR:" + folders.get(L).getNodeRef()); + searchQueryService.expectResultsFromQuery(queryAncestorL, testUser, + // primary descendant + folders.get(M).getName(), + // secondary descendants + folders.get(C).getName(), + folders.get(Y).getName(), + folders.get(Z).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testSecondaryAncestorWithDocumentAsSecondaryChild() + { + // then + STEP("Verify that searching by ANCESTOR and folderA will find nodes: folderB, folderC and fileInP."); + SearchRequest query = req("ANCESTOR:" + folders.get(A).getNodeRef()); + searchQueryService.expectResultsFromQuery(query, testUser, + // primary descendants + folders.get(B).getName(), + folders.get(C).getName(), + // secondary descendant + fileInP.getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testSecondaryAncestorWithNodeHavingComplexSecondaryRelationship() + { + // then + STEP("Verify that all descendant of folderX can be found."); + SearchRequest query = req("ANCESTOR:" + folders.get(X).getNodeRef()); + searchQueryService.expectResultsFromQuery(query, testUser, + // primary descendants + folders.get(Y).getName(), + folders.get(Z).getName(), + // secondary descendants + folders.get(B).getName(), + folders.get(C).getName(), + folders.get(K).getName(), + folders.get(L).getName(), + folders.get(M).getName()); + } + + /** + * Verify that removing secondary parent-child relationship will result in updating ES index: ANCESTOR. Test changes below folders hierarchy: + * + *
+     * DL
+     *  += fQ
+     *     +
+     *     |
+     *  += fR
+     * 
+ * + * into: + * + *
+     * DL += fQ += fR
+     * 
+ */ + @Test(groups = TestGroup.SEARCH) + public void testSecondaryAncestorWithDeletedSecondaryRelationship() + { + // given + STEP("Add to folderQ a secondary child folderR and verify if it can be found using ANCESTOR index and secondary child node reference."); + folders.modify(Q).add().secondaryContent(folders.get(R)); + + STEP("Verify that searching by ANCESTOR and folderQ will find secondary descendant node: folderR."); + SearchRequest query = req("ANCESTOR:" + folders.get(Q).getNodeRef()); + searchQueryService.expectResultsFromQuery(query, testUser, + // secondary descendant + folders.get(R).getName()); + + // when + STEP("Delete the secondary parent-child relationship between folderQ and FolderR."); + folders.modify(Q).remove().secondaryContent(folders.get(R)); + + // then + STEP("Verify that folderQ cannot be found by ANCESTOR and folderQ anymore."); + searchQueryService.expectNoResultsFromQuery(query, testUser); + } + + /** + * Verify that removing a node D (fD) having a secondary children relationship will remove the relationships and update ANCESTOR index in ES. Test changes below folders hierarchy from: + * + *
+     * DL
+     *  += fQ
+     *     +
+     *     |
+     *  += fE += fF
+     *     +
+     *     |
+     *  += fR
+     * 
+ * + * into: + * + *
+     * DL += fQ += fR
+     * 
+ */ + @Test(groups = TestGroup.SEARCH) + public void testSecondaryAncestorWithDeletedSecondaryParentNode() + { + // given + STEP("Create two nested folders (E and F) in Document Library."); + FolderModel folderE = folders.add().randomFolder("E").create(); + FolderModel folderF = folders.modify(folderE).add().randomFolder("F").create(); + STEP("Make folderE a secondary children of folderQ and folderR a secondary children of folderE."); + folders.modify(Q).add().secondaryContent(folderE); + folders.modify(folderE).add().secondaryContent(folders.get(R)); + + STEP("Verify that searching by ANCESTOR and folderQ will find its secondary descendant: folderE, folderF and folderR."); + SearchRequest queryAncestorQ = req("ANCESTOR:" + folders.get(Q).getNodeRef()); + searchQueryService.expectResultsFromQuery(queryAncestorQ, testUser, + // secondary descendants + folderE.getName(), + folderF.getName(), + folders.get(R).getName()); + + // when + STEP("Delete folderE with its content."); + folders.modify(folderE).delete(); + + // then + STEP("Verify that searching by ANCESTOR and folderQ will not find any nodes."); + searchQueryService.expectNoResultsFromQuery(queryAncestorQ, testUser); + } + + /** + * Verify that moving folderD (fD) containing secondary children from hierarchy: + * + *
+     * DL
+     *  += fQ += fD +- fP += file
+     *  += fR
+     * 
+ * + * to: + * + *
+     * DL
+     *  += fQ
+     *  += fR += fD +- fP += file
+     * 
+ * + * will update ANCESTOR index in ES. + */ + @Test(groups = TestGroup.SEARCH) + public void testSecondaryAncestorWithMovedSecondaryParentNode() + { + // given + STEP("Create folderD inside folderQ, and add folderP to D as a secondary child."); + FolderModel folderD = folders.modify(Q).add().randomFolder("D").create(); + folders.modify(folderD).add().secondaryContent(folders.get(P)); + + STEP("Verify that searching by ANCESTOR and folderQ will find its primary and secondary descendant nodes: folderD, folderP and file."); + SearchRequest queryAncestorQ = req("ANCESTOR:" + folders.get(Q).getNodeRef()); + searchQueryService.expectResultsFromQuery(queryAncestorQ, testUser, + // primary descendant + folderD.getName(), + // secondary descendants + folders.get(P).getName(), + fileInP.getName()); + STEP("Verify that searching by ANCESTOR and folderR will not find any descendant nodes."); + SearchRequest queryAncestorR = req("ANCESTOR:" + folders.get(R).getNodeRef()); + searchQueryService.expectNoResultsFromQuery(queryAncestorR, testUser); + + // when + STEP("Move folderD from folderQ to folderR."); + folders.modify(folderD).moveTo(folders.get(R)); + + // then + STEP("Verify that search result for ANCESTOR and folderQ will not find any descendant anymore."); + searchQueryService.expectNoResultsFromQuery(queryAncestorQ, testUser); + STEP("Verify that searching by ANCESTOR and folderR will find its primary and secondary descendant nodes: folderD, folderP and file."); + searchQueryService.expectResultsFromQuery(queryAncestorR, testUser, + // primary descendant + folderD.getName(), + // secondary descendants + folders.get(P).getName(), + fileInP.getName()); + + STEP("Clean-up - delete folderD."); + folders.modify(folderD).delete(); + } + + /** + * Verify that copying folder will also result in copying folder's secondary children and update ANCESTOR index in ES. Test changes below folders hierarchy: + * + *
+     * DL
+     *  += fS += fG += fH
+     *         +
+     *        /
+     *  += fP += file
+     *  += fT
+     * 
+ * + * into: + * + *
+     * DL
+     *  += fS += fG += fH
+     *         +
+     *        /
+     *  += fP += file
+     *        \
+     *         +
+     *  += fT += fG-c += fH-c
+     * 
+ */ + @Test(groups = TestGroup.SEARCH) + public void testSecondaryAncestorWithCopiedSecondaryParentNode() throws InterruptedException + { + // given + STEP("Create nested folders (G and H) inside folderS and folderT in Document Library. Make folderP a secondary child of folderG."); + FolderModel folderG = folders.modify(S).add().randomFolder("G").create(); + FolderModel folderH = folders.modify(folderG).add().randomFolder("H").create(); + FolderModel folderT = folders.add().randomFolder("T").create(); + folders.modify(folderG).add().secondaryContent(folders.get(P)); + + STEP("Verify that searching by ANCESTOR and folderS will find its descendant nodes: folderG, folderH, folderP and file in P."); + SearchRequest queryAncestorS = req("ANCESTOR:" + folders.get(S).getNodeRef()); + Utility.sleep(500, 60000, () -> searchQueryService.expectResultsFromQuery(queryAncestorS, testUser, + // primary descendants + folderG.getName(), + folderH.getName(), + // secondary descendants + folders.get(P).getName(), + fileInP.getName())); + STEP("Verify that searching by ANCESTOR and folderT will not find any nodes."); + SearchRequest queryAncestorT = req("ANCESTOR:" + folderT.getNodeRef()); + searchQueryService.expectNoResultsFromQuery(queryAncestorT, testUser); + + // when + STEP("Copy folderG with its content to folderT."); + FolderModel folderGCopy = folders.modify(folderG).copyTo(folderT); + + // then + STEP("Verify that searching by ANCESTOR and folderS will find its descendant nodes: folderG, folderH, folderP and file in P."); + searchQueryService.expectResultsFromQuery(queryAncestorS, testUser, + // primary descendants + folderG.getName(), + folderH.getName(), + // secondary descendants + folders.get(P).getName(), + fileInP.getName()); + STEP("Verify that searching by ANCESTOR and folderT will find its descendant nodes: folderG-copy, folderH-copy, folderP, file."); + searchQueryService.expectResultsFromQuery(queryAncestorT, testUser, + // primary descendants + folderGCopy.getName(), + folderH.getName(), // the same name as folderH-copy + // secondary descendants + folders.get(P).getName(), + fileInP.getName()); + + STEP("Clean-up - delete folderG and folderT (with G's copy)."); + folders.modify(folderG).delete(); + folders.modify(folderT).delete(); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodesSecondaryChildrenRelatedTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodesSecondaryChildrenRelatedTests.java new file mode 100644 index 000000000..278815ef6 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodesSecondaryChildrenRelatedTests.java @@ -0,0 +1,90 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.alfresco.utility.report.log.Step.STEP; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; + +import org.alfresco.rest.core.RestWrapper; +import org.alfresco.rest.repo.resource.Folders; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; + +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +public abstract class NodesSecondaryChildrenRelatedTests extends AbstractTestNGSpringContextTests +{ + @SuppressWarnings("PMD.OneDeclarationPerLine") + protected static final String A = "A", B = "B", C = "C", K = "K", L = "L", M = "M", P = "P", Q = "Q", R = "R", S = "S", X = "X", Y = "Y", Z = "Z"; + + @Autowired + private ServerHealth serverHealth; + @Autowired + private DataUser dataUser; + @Autowired + private DataSite dataSite; + @Autowired + protected DataContent dataContent; + @Autowired + protected RestWrapper restClient; + @Autowired + protected SearchQueryService searchQueryService; + + protected UserModel testUser; + protected SiteModel testSite; + protected Folders folders; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + STEP("Verify environment health."); + serverHealth.isServerReachable(); + serverHealth.assertServerIsOnline(); + + STEP("Create a test user and private site."); + testUser = dataUser.createRandomTestUser(); + testSite = dataSite.usingUser(testUser).createPrivateRandomSite(); + folders = new Folders(dataContent, restClient, testUser, testSite); + } + + @AfterClass(alwaysRun = true) + public void dataCleanUp() + { + STEP("Clean up data after tests."); + dataSite.usingUser(testUser).deleteSite(testSite); + dataUser.usingAdmin().deleteUser(testUser); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodesSecondaryParentIndexingTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodesSecondaryParentIndexingTests.java new file mode 100644 index 000000000..37e39392f --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodesSecondaryParentIndexingTests.java @@ -0,0 +1,361 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.alfresco.elasticsearch.SearchQueryService.req; +import static org.alfresco.utility.report.log.Step.STEP; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.TestGroup; + +/** + * Tests verifying indexing of secondary children and PARENT index in Elasticsearch. + */ +@SuppressWarnings({"PMD.JUnitTestsShouldIncludeAssert"}) // these are TAS E2E tests and use searchQueryService.expectResultsFromQuery for assertion +public class NodesSecondaryParentIndexingTests extends NodesSecondaryChildrenRelatedTests +{ + + private FileModel fileInP; + + /** + * Creates a user and a private site containing below hierarchy of folders. + * + *
+     * Site
+     * DL (Document Library)
+     *  += fA += fB += fC (folderC)
+     *         /     / |
+     *        +     +  +
+     *  += fK += fL += fM
+     *     |     +
+     *     +     |
+     *  += fX += fY += fZ
+     *  += fP += file -+ fA
+     *  += fQ
+     *  += fR
+     *  += fS
+     * 
+ * + * Parent += Child - primary parent-child relationship Parent +- Child - secondary parent-child relationship + */ + @BeforeClass(alwaysRun = true) + @Override + public void dataPreparation() + { + super.dataPreparation(); + + // given + STEP("Create few sets of nested folders in site's Document Library."); + folders.add().nestedRandomFolders(A, B, C).create(); + folders.add().nestedRandomFolders(K, L, M).create(); + folders.add().nestedRandomFolders(X, Y, Z).create(); + folders.add().randomFolders(P, Q, R, S).create(); + fileInP = folders.modify(P).add().randomFile().create(); + + STEP("Create few secondary parent-child relationships."); + folders.modify(K).add().secondaryContent(folders.get(B)); + folders.modify(X).add().secondaryContent(folders.get(K)); + folders.modify(L).add().secondaryContent(folders.get(C), folders.get(Y)); + folders.modify(M).add().secondaryContent(folders.get(C)); + folders.modify(A).add().secondaryContent(fileInP); + } + + @Test(groups = TestGroup.SEARCH) + public void testSecondaryParentWithNodeHavingOneSecondaryChild() + { + // then + STEP("Verify that searching by PARENT and folderM will find node folderC."); + SearchRequest query = req("PARENT:" + folders.get(M).getNodeRef()); + searchQueryService.expectResultsFromQuery(query, testUser, + folders.get(C).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testSecondaryParentWithNodeHavingTwoSecondaryChildren() + { + // then + STEP("Verify that searching by PARENT and folderL will find nodes: folderM, FolderC and folderY."); + SearchRequest query = req("PARENT:" + folders.get(L).getNodeRef()); + searchQueryService.expectResultsFromQuery(query, testUser, + // primary child + folders.get(M).getName(), + // secondary children + folders.get(C).getName(), + folders.get(Y).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testSecondaryParentWithDocumentAsSecondaryChild() + { + // then + STEP("Verify that searching by PARENT and folderA will find nodes: folderB and file."); + SearchRequest query = req("PARENT:" + folders.get(A).getNodeRef()); + searchQueryService.expectResultsFromQuery(query, testUser, + // primary child + folders.get(B).getName(), + // secondary child + fileInP.getName()); + } + + /** + * Verify that removing secondary parent-child relationship will result in updating ES index: PARENT. Test changes below folders hierarchy: + * + *
+     * DL
+     *  += fQ
+     *     +
+     *     |
+     *  += fR
+     * 
+ * + * into: + * + *
+     * DL += fQ += fR
+     * 
+ */ + @Test(groups = TestGroup.SEARCH) + public void testSecondaryParentWithDeletedSecondaryRelationship() + { + // given + STEP("Add to folderQ a secondary child folderR and verify if it can be found using PARENT index and secondary parent node reference."); + folders.modify(Q).add().secondaryContent(folders.get(R)); + + SearchRequest query = req("PARENT:" + folders.get(Q).getNodeRef()); + searchQueryService.expectResultsFromQuery(query, testUser, + // secondary child + folders.get(R).getName()); + + // when + STEP("Delete the secondary parent relationship between folderQ and FolderR and verify that folderR cannot be found by PARENT and folderQ anymore."); + folders.modify(Q).remove().secondaryContent(folders.get(R)); + + // then + searchQueryService.expectNoResultsFromQuery(query, testUser); + } + + /** + * Verify that removing a node D (fD) having a secondary children relationship will remove the relationships and update PARENT index in ES. Test changes below folders hierarchy from: + * + *
+     * DL
+     *  += fQ
+     *     +
+     *     |
+     *  += fE += fF
+     *     +
+     *     |
+     *  += fR
+     * 
+ * + * into: + * + *
+     * DL += fQ += fR
+     * 
+ */ + @Test(groups = TestGroup.SEARCH) + public void testSecondaryParentWithDeletedSecondaryParentNode() + { + // given + STEP("Create two nested folders (E and F) in Document Library."); + FolderModel folderE = folders.add().randomFolder("E").create(); + FolderModel folderF = folders.modify(folderE).add().randomFolder("F").create(); + STEP("Make folderE a secondary children of folderQ and folderR a secondary children of folderE."); + folders.modify(Q).add().secondaryContent(folderE); + folders.modify(folderE).add().secondaryContent(folders.get(R)); + + STEP("Verify that searching by PARENT and folderQ will find its secondary child: folderE."); + SearchRequest queryParentQ = req("PARENT:" + folders.get(Q).getNodeRef()); + searchQueryService.expectResultsFromQuery(queryParentQ, testUser, + // secondary child + folderE.getName()); + STEP("Verify that searching by PARENT and folderE will find its primary and secondary children: folderF and folderR."); + SearchRequest queryParentD = req("PARENT:" + folderE.getNodeRef()); + searchQueryService.expectResultsFromQuery(queryParentD, testUser, + // primary child + folderF.getName(), + // secondary child + folders.get(R).getName()); + + // when + STEP("Delete folderE and verify that PARENT was updated for nodes folderQ and folderR."); + folders.modify(folderE).delete(); + + // then + searchQueryService.expectNoResultsFromQuery(queryParentQ, testUser); + searchQueryService.expectNoResultsFromQuery(queryParentD, testUser); + } + + /** + * Verify that moving folder D (fD) containing secondary children from hierarchy: + * + *
+     * DL
+     *  += fQ += fD +- fP += file
+     *  += fR
+     * 
+ * + * to: + * + *
+     * DL
+     *  += fQ
+     *  += fR += fD +- fP += file
+     * 
+ * + * will update PARENT index in ES. + */ + @Test(groups = TestGroup.SEARCH) + public void testSecondaryParentWithMovedSecondaryParentNode() + { + // given + STEP("Create folderD inside folderQ, and add folderP to D as a secondary child."); + FolderModel folderD = folders.modify(Q).add().randomFolder("D").create(); + folders.modify(folderD).add().secondaryContent(folders.get(P)); + + STEP("Verify that searching by PARENT and folderD will find node folderP."); + SearchRequest queryParentD = req("PARENT:" + folderD.getNodeRef()); + searchQueryService.expectResultsFromQuery(queryParentD, testUser, + // secondary child + folders.get(P).getName()); + STEP("Verify that searching by PARENT and folderQ will find node folderD."); + SearchRequest queryParentQ = req("PARENT:" + folders.get(Q).getNodeRef()); + searchQueryService.expectResultsFromQuery(queryParentQ, testUser, + // primary child + folderD.getName()); + + // when + STEP("Move folderD from folderQ to folderR."); + folders.modify(folderD).moveTo(folders.get(R)); + + // then + STEP("Verify that search result for PARENT and folderD didn't change."); + searchQueryService.expectResultsFromQuery(queryParentD, testUser, + // secondary child + folders.get(P).getName()); + STEP("Verify that searching by PARENT and folderQ doesn't return any node anymore."); + searchQueryService.expectNoResultsFromQuery(queryParentQ, testUser); + STEP("Verify that searching by PARENT and folderR will find node folderD."); + SearchRequest queryParentR = req("PARENT:" + folders.get(R).getNodeRef()); + searchQueryService.expectResultsFromQuery(queryParentR, testUser, + // primary child + folderD.getName()); + + STEP("Clean-up - delete folderD."); + folders.modify(folderD).delete(); + } + + /** + * Verify that copying folder will also result in copying folder's secondary children and update PARENT index in ES. Test changes below folders hierarchy: + * + *
+     * DL
+     *  += fS += fG += fH
+     *         +
+     *        /
+     *  += fP += file
+     *  += fT
+     * 
+ * + * into: + * + *
+     * DL
+     *  += fS += fG += fH
+     *         +
+     *        /
+     *  += fP += file
+     *        \
+     *         +
+     *  += fT += fG-c += fH-c
+     * 
+ */ + @Test(groups = TestGroup.SEARCH) + public void testSecondaryParentWithCopiedSecondaryParentNode() + { + // given + STEP("Create nested folders (G and H) inside folderS and folderT in Document Library. Make folderP a secondary child of folderG."); + FolderModel folderG = folders.modify(S).add().randomFolder("G").create(); + FolderModel folderH = folders.modify(folderG).add().randomFolder("H").create(); + FolderModel folderT = folders.add().randomFolder("T").create(); + folders.modify(folderG).add().secondaryContent(folders.get(P)); + + STEP("Verify that searching by PARENT and folderG will find nodes: folderH, folderP and file."); + SearchRequest queryParentG = req("PARENT:" + folderG.getNodeRef()); + searchQueryService.expectResultsFromQuery(queryParentG, testUser, + // primary child + folderH.getName(), + // secondary child + folders.get(P).getName()); + STEP("Verify that searching by PARENT and folderS will find nodes: folderG, folderH, folderP and file."); + SearchRequest queryParentS = req("PARENT:" + folders.get(S).getNodeRef()); + searchQueryService.expectResultsFromQuery(queryParentS, testUser, + // primary child + folderG.getName()); + STEP("Verify that searching by PARENT and folderP will find node: file."); + SearchRequest queryParentP = req("PARENT:" + folders.get(P).getNodeRef()); + searchQueryService.expectResultsFromQuery(queryParentP, testUser, + // primary child + fileInP.getName()); + + // when + STEP("Copy folderG with its content to folderT."); + FolderModel folderGCopy = folders.modify(folderG).copyTo(folderT); + + // then + STEP("Verify that search result for PARENT and folderS didn't change."); + searchQueryService.expectResultsFromQuery(queryParentS, testUser, + // primary child + folderG.getName()); + STEP("Verify that searching by PARENT and folderS/folderG will find nodes: folderH, folderP and file in P."); + searchQueryService.expectResultsFromQuery(queryParentG, testUser, + // primary child + folderH.getName(), + // secondary child + folders.get(P).getName()); + STEP("Verify that folderG was copied with secondary parent-child relationship and PARENT reflects that - search by folderT/folderGCopy should find nodes: folderH, folderP and file in P."); + SearchRequest queryParentGCopy = req("PARENT:" + folderGCopy.getNodeRef()); + searchQueryService.expectResultsFromQuery(queryParentGCopy, testUser, + // primary child + folderH.getName(), // name is the same as folderH-copy + // secondary child + folders.get(P).getName()); + STEP("Verify that this time searching by PARENT and folderP will find node: file."); + searchQueryService.expectResultsFromQuery(queryParentP, testUser, + // primary child + fileInP.getName()); + + STEP("Clean-up - delete folderG and folderT (with G's copy)."); + folders.modify(folderG).delete(); + folders.modify(folderT).delete(); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodesSecondaryPathIndexingTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodesSecondaryPathIndexingTests.java new file mode 100644 index 000000000..fc2a6b551 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/NodesSecondaryPathIndexingTests.java @@ -0,0 +1,392 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.alfresco.elasticsearch.SearchQueryService.req; +import static org.alfresco.utility.report.log.Step.STEP; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.TestGroup; + +/** + * Tests verifying indexing of secondary children and PATH index in Elasticsearch. + */ +@SuppressWarnings({"PMD.JUnitTestsShouldIncludeAssert"}) // these are TAS E2E tests and use searchQueryService.expectResultsFromQuery for assertion +public class NodesSecondaryPathIndexingTests extends NodesSecondaryChildrenRelatedTests +{ + + private FileModel fileInP; + + /** + * Creates a user and a private site containing below hierarchy of folders. + * + *
+     * Site
+     * DL (Document Library)
+     *  += fA += fB += fC (folderC)
+     *         /     / |
+     *        +     +  +
+     *  += fK += fL += fM
+     *     |     +
+     *     +     |
+     *  += fX += fY += fZ
+     *  += fP += file -+ fA
+     *  += fQ
+     *  += fR
+     *  += fS
+     * 
+ * + * Parent += Child - primary parent-child relationship Parent +- Child - secondary parent-child relationship + */ + @BeforeClass(alwaysRun = true) + @Override + public void dataPreparation() + { + super.dataPreparation(); + + // given + STEP("Create few sets of nested folders in site's Document Library."); + folders.add().nestedRandomFolders(A, B, C).create(); + folders.add().nestedRandomFolders(K, L, M).create(); + folders.add().nestedRandomFolders(X, Y, Z).create(); + folders.add().randomFolders(P, Q, R, S).create(); + fileInP = folders.modify(P).add().randomFile().create(); + + STEP("Create few secondary parent-child relationships."); + folders.modify(K).add().secondaryContent(folders.get(B)); + folders.modify(X).add().secondaryContent(folders.get(K)); + folders.modify(L).add().secondaryContent(folders.get(C), folders.get(Y)); + folders.modify(M).add().secondaryContent(folders.get(C)); + folders.modify(A).add().secondaryContent(fileInP); + + STEP("Add to folderQ a secondary child folderR."); + folders.modify(Q).add().secondaryContent(folders.get(R)); + + // when + STEP("Delete the secondary parent relationship between folderQ and FolderR."); + folders.modify(Q).remove().secondaryContent(folders.get(R)); + } + + @Test(groups = TestGroup.SEARCH) + public void testSecondaryPathWithNodeHavingOneSecondaryChild() + { + // then + STEP("Verify that folderC can be found by secondary PATH using secondary parent folderM."); + SearchRequest query = req("PATH:\"//cm:" + folders.get(M).getName() + "//*\""); + searchQueryService.expectResultsFromQuery(query, testUser, + // secondary path + folders.get(C).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testSecondaryPathWithNodeHavingOnePrimaryAndTwoSecondaryChildren() + { + // then + STEP("Verify that primary and secondary children of folderL can be found using PATH index."); + SearchRequest query = req("PATH:\"//cm:" + folders.get(L).getName() + "//*\""); + searchQueryService.expectResultsFromQuery(query, testUser, + // primary path + folders.get(M).getName(), + // secondary path + folders.get(C).getName(), + folders.get(Y).getName(), + folders.get(Z).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testSecondaryPathWithNodeHavingDocumentAsSecondaryChild() + { + // then + STEP("Verify that a file being a secondary child of folderA can be found using PATH index."); + SearchRequest query = req("PATH:\"//cm:" + folders.get(A).getName() + "//*\""); + searchQueryService.expectResultsFromQuery(query, testUser, + // primary path + folders.get(B).getName(), + folders.get(C).getName(), + // secondary path + fileInP.getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testSecondaryPathWithNodeHavingComplexSecondaryRelationship() + { + // then + STEP("Verify that all secondary children of folderX can be found."); + SearchRequest query = req("PATH:\"//cm:" + folders.get(X).getName() + "//*\""); + searchQueryService.expectResultsFromQuery(query, testUser, + // primary path + folders.get(Y).getName(), + folders.get(Z).getName(), + // secondary path + folders.get(B).getName(), + folders.get(C).getName(), + folders.get(K).getName(), + folders.get(L).getName(), + folders.get(M).getName()); + } + + /** + * Verify that removing secondary parent-child relationship will result in updating ES index: PATH. Test changes below folders hierarchy: + * + *
+     * DL
+     *  += fQ
+     *     +
+     *     |
+     *  += fR
+     * 
+ * + * into: + * + *
+     * DL += fQ += fR
+     * 
+ */ + @Test(groups = TestGroup.SEARCH) + public void testSecondaryPathWithDeletedSecondaryRelationship() + { + // then + STEP("Verify that folderR cannot be found by PATH and folderQ anymore."); + SearchRequest query = req("PATH:\"//cm:" + folders.get(Q).getName() + "//*\""); + searchQueryService.expectNoResultsFromQuery(query, testUser); + } + + /** + * Verify that removing a node D (fD) having a secondary children relationship will remove the relationships and update PATH index in ES. Test changes below folders hierarchy from: + * + *
+     * DL
+     *  += fQ
+     *     +
+     *     |
+     *  += fE += fF
+     *     +
+     *     |
+     *  += fR
+     * 
+ * + * into: + * + *
+     * DL += fQ += fR
+     * 
+ */ + @Test(groups = TestGroup.SEARCH) + public void testSecondaryPathWithDeletedSecondaryParentNode() + { + // given + STEP("Create two nested folders (E and F) in Document Library."); + FolderModel folderE = folders.add().randomFolder("E").create(); + FolderModel folderF = folders.modify(folderE).add().randomFolder("F").create(); + STEP("Make folderE a secondary children of folderQ and folderR a secondary children of folderE."); + folders.modify(Q).add().secondaryContent(folderE); + folders.modify(folderE).add().secondaryContent(folders.get(R)); + + STEP("Verify that searching by PATH and folderQ will find nodes: folderE, folderF and folderR."); + SearchRequest queryPathQ = req("PATH:\"//cm:" + folders.get(Q).getName() + "//*\""); + searchQueryService.expectResultsFromQuery(queryPathQ, testUser, + // secondary path + folderE.getName(), + folderF.getName(), + folders.get(R).getName()); + STEP("Verify that searching by PATH and folderE will find its primary and secondary children: folderF and folderR."); + SearchRequest queryPathD = req("PATH:\"//cm:" + folderE.getName() + "//*\""); + searchQueryService.expectResultsFromQuery(queryPathD, testUser, + // primary path + folderF.getName(), + // secondary path + folders.get(R).getName()); + + // when + STEP("Delete folderE and verify that PATH was updated for nodes folderQ and folderR."); + folders.modify(folderE).delete(); + + // then + searchQueryService.expectNoResultsFromQuery(queryPathQ, testUser); + searchQueryService.expectNoResultsFromQuery(queryPathD, testUser); + } + + /** + * Verify that moving folder D (fD) containing secondary children from hierarchy: + * + *
+     * DL
+     *  += fQ += fD +- fP += file
+     *  += fR
+     * 
+ * + * to: + * + *
+     * DL
+     *  += fQ
+     *  += fR += fD +- fP += file
+     * 
+ * + * will update PATH index in ES. + */ + @Test(groups = TestGroup.SEARCH) + public void testSecondaryPathWithMovedSecondaryParentNode() + { + // given + STEP("Create folderD inside folderQ, and add folderP as a secondary child."); + FolderModel folderD = folders.modify(Q).add().randomFolder("D").create(); + folders.modify(folderD).add().secondaryContent(folders.get(P)); + folders.modify(M).add().secondaryContent(folderD); + + STEP("Verify that searching by PATH and folderD will find nodes: folderP and file."); + SearchRequest queryPathD = req("PATH:\"//cm:" + folderD.getName() + "//*\""); + searchQueryService.expectResultsFromQuery(queryPathD, testUser, + // secondary path + folders.get(P).getName(), + fileInP.getName()); + STEP("Verify that searching by PATH and folderQ will find nodes: folderD, folderP and file."); + SearchRequest queryPathQ = req("PATH:\"//cm:" + folders.get(Q).getName() + "//*\""); + searchQueryService.expectResultsFromQuery(queryPathQ, testUser, + // primary path + folderD.getName(), + // secondary path + folders.get(P).getName(), + fileInP.getName()); + + // when + STEP("Move folderD from folderQ to folderR."); + folders.modify(folderD).moveTo(folders.get(R)); + + // then + STEP("Verify that search result for PATH and folderD didn't change."); + searchQueryService.expectResultsFromQuery(queryPathD, testUser, + // secondary path + folders.get(P).getName(), + fileInP.getName()); + STEP("Verify that searching by PATH and folderQ doesn't return any node anymore."); + searchQueryService.expectNoResultsFromQuery(queryPathQ, testUser); + STEP("Verify that searching by PATH and folderR will find nodes: folderD, folderP and file."); + SearchRequest queryPathR = req("PATH:\"//cm:" + folders.get(R).getName() + "//*\""); + searchQueryService.expectResultsFromQuery(queryPathR, testUser, + // primary path + folderD.getName(), + // secondary path + folders.get(P).getName(), + fileInP.getName()); + + STEP("Clean-up - delete folderD."); + folders.modify(folderD).delete(); + } + + /** + * Verify that copying folder will also result in copying folder's secondary children and update PATH index in ES. Test changes below folders hierarchy: + * + *
+     * DL
+     *  += fS += fG += fH
+     *         +
+     *        /
+     *  += fP += file
+     *  += fT
+     * 
+ * + * into: + * + *
+     * DL
+     *  += fS += fG += fH
+     *         +
+     *        /
+     *  += fP += file
+     *        \
+     *         +
+     *  += fT += fG-c += fH-c
+     * 
+ */ + @Test(groups = TestGroup.SEARCH) + public void testSecondaryParentWithCopiedSecondaryParentNode() + { + // given + STEP("Create nested folders (G and H) inside folderS and folderT in Document Library. Make folderP a secondary child of folderG."); + FolderModel folderG = folders.modify(S).add().randomFolder("G").create(); + FolderModel folderH = folders.modify(folderG).add().randomFolder("H").create(); + FolderModel folderT = folders.add().randomFolder("T").create(); + folders.modify(folderG).add().secondaryContent(folders.get(P)); + + STEP("Verify that searching by PATH and folderG will find nodes: folderH, folderP and file."); + SearchRequest queryPathG = req("PATH:\"//cm:" + folderG.getName() + "//*\""); + searchQueryService.expectResultsFromQuery(queryPathG, testUser, + // primary path + folderH.getName(), + // secondary path + folders.get(P).getName(), + fileInP.getName()); + STEP("Verify that searching by PATH and folderS will find nodes: folderG, folderH, folderP and file."); + SearchRequest queryPathS = req("PATH:\"//cm:" + folders.get(S).getName() + "//*\""); + searchQueryService.expectResultsFromQuery(queryPathS, testUser, + // primary path + folderG.getName(), + folderH.getName(), + // secondary path + folders.get(P).getName(), + fileInP.getName()); + + // when + STEP("Copy folderG with its content to folderT."); + FolderModel folderGCopy = folders.modify(folderG).copyTo(folderT); + + // then + STEP("Verify that search result for PATH and folderS didn't change."); + searchQueryService.expectResultsFromQuery(queryPathS, testUser, + // primary path + folderH.getName(), + folderG.getName(), + // secondary path + folders.get(P).getName(), + fileInP.getName()); + STEP("Verify that searching by PATH and folderS/folderG will find nodes: folderH, folderP and file in P."); + SearchRequest queryPathSG = req("PATH:\"//cm:" + folders.get(S).getName() + "/cm:" + folderG.getName() + "//*\""); + searchQueryService.expectResultsFromQuery(queryPathSG, testUser, + // primary path + folderH.getName(), + // secondary path + folders.get(P).getName(), + fileInP.getName()); + STEP("Verify that folderG was copied with secondary parent-child relationship and PATH reflects that - search by folderT/folderGCopy should find nodes: folderH, folderP and file in P."); + SearchRequest queryPathTGCopy = req("PATH:\"//cm:" + folderT.getName() + "/cm:" + folderGCopy.getName() + "//*\""); + searchQueryService.expectResultsFromQuery(queryPathTGCopy, testUser, + // primary path + folderH.getName(), // the same name as folderH-copy + // secondary path + folders.get(P).getName(), + fileInP.getName()); + + STEP("Clean-up - delete folderG and folderT (with G's copy)."); + folders.modify(folderG).delete(); + folders.modify(folderT).delete(); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/PathFieldsIndexingTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/PathFieldsIndexingTests.java new file mode 100644 index 000000000..d4d68d9a9 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/PathFieldsIndexingTests.java @@ -0,0 +1,174 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.alfresco.elasticsearch.SearchQueryService.req; +import static org.alfresco.utility.report.log.Step.STEP; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.utility.Utility; +import org.alfresco.utility.model.TestGroup; + +@SuppressWarnings({"PMD.JUnitTestsShouldIncludeAssert"}) // these are TAS E2E tests and use searchQueryService.expectResultsFromQuery for assertion +public class PathFieldsIndexingTests extends NodesSecondaryChildrenRelatedTests +{ + /** + * Creates a user and a private site containing below hierarchy of folders. + * + *
+     * Site
+     * DL (Document Library)
+     *  += fA += fB += fC (folderC)
+     *         /     / |
+     *        +     +  +
+     *  += fK += fL += fM
+     *     |     +
+     *     +     |
+     *  += fX += fY += fZ
+     * 
+ * + * Parent += Child - primary parent-child relationship Parent +- Child - secondary parent-child relationship + */ + @BeforeClass(alwaysRun = true) + @Override + public void dataPreparation() + { + super.dataPreparation(); + + // given + STEP("Create some nested folders."); + folders.add().nestedRandomFolders(A, B, C).create(); + folders.add().nestedRandomFolders(K, L, M).create(); + folders.add().nestedRandomFolders(X, Y, Z).create(); + + STEP("Add some extra secondary parent relationships."); + folders.modify(K).add().secondaryContent(folders.get(B)); + folders.modify(X).add().secondaryContent(folders.get(K)); + folders.modify(L).add().secondaryContent(folders.get(C), folders.get(Y)); + folders.modify(M).add().secondaryContent(folders.get(C)); + + STEP("Wait for the batch indexer to index the last secondary association (M +- C)."); + SearchRequest indexingProbe = req("PARENT:" + folders.get(M).getNodeRef()); + try + { + Utility.sleep(500, 60000, () -> searchQueryService.expectResultsFromQuery( + indexingProbe, testUser, folders.get(C).getName())); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + throw new AssertionError("Interrupted while waiting for batch indexer", e); + } + } + + @Test(groups = TestGroup.SEARCH) + public void testPrimaryParentField() + { + STEP("Check that only M has L as a primary parent."); + SearchRequest query = req("PRIMARYPARENT:" + folders.get(L).getNodeRef()); + searchQueryService.expectResultsFromQuery(query, testUser, + folders.get(M).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testParentFieldIncludesSecondaryParents() + { + STEP("Check that three nodes have L as a primary or secondary parent."); + SearchRequest query = req("PARENT:" + folders.get(L).getNodeRef()); + searchQueryService.expectResultsFromQuery(query, testUser, + folders.get(C).getName(), folders.get(M).getName(), folders.get(Y).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testAncestorFieldIncludesSecondaryAssociations() + { + STEP("Check which nodes have K as an ancestor."); + SearchRequest query = req("ANCESTOR:" + folders.get(K).getNodeRef()); + searchQueryService.expectResultsFromQuery(query, testUser, + folders.get(B).getName(), folders.get(C).getName(), folders.get(L).getName(), folders.get(M).getName(), + folders.get(Y).getName(), folders.get(Z).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testSecondaryPathWithNodeHavingOneSecondaryChild() + { + // then + STEP("Verify that folderC can be found by secondary PATH using secondary parent folderM."); + SearchRequest query = req("PATH:\"//cm:" + folders.get(M).getName() + "//*\""); + searchQueryService.expectResultsFromQuery(query, testUser, + // secondary path + folders.get(C).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testSecondaryPathWithNodeHavingOnePrimaryAndTwoSecondaryChildren() + { + // then + STEP("Verify that primary and secondary children of folderL can be found using PATH index."); + SearchRequest query = req("PATH:\"//cm:" + folders.get(L).getName() + "//*\""); + searchQueryService.expectResultsFromQuery(query, testUser, + // primary path + folders.get(M).getName(), + // secondary path + folders.get(C).getName(), + folders.get(Y).getName(), + folders.get(Z).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testQueryThroughSecondaryPath() + { + // then + STEP("Verify we can get direct children of a secondary paths (X-K=L) referenced in a query."); + SearchRequest query = req("PATH:\"//cm:" + folders.get(X).getName() + "//cm:" + folders.get(L).getName() + "/*\""); + searchQueryService.expectResultsFromQuery(query, testUser, + // primary path from L + folders.get(M).getName(), + // secondary path from L + folders.get(C).getName(), + folders.get(Y).getName()); + } + + @Test(groups = TestGroup.SEARCH) + public void testSecondaryPathWithNodeHavingComplexSecondaryRelationship() + { + // then + STEP("Verify we can find all secondary descendents (excluding direct children) of folderX."); + SearchRequest query = req("PATH:\"//cm:" + folders.get(X).getName() + "//*//*\""); + searchQueryService.expectResultsFromQuery(query, testUser, + // primary path + folders.get(Z).getName(), + // secondary path + folders.get(Y).getName(), // Y _is_ a direct primary child, but this query should only find it via the secondary path. + folders.get(B).getName(), + folders.get(C).getName(), + folders.get(L).getName(), + folders.get(M).getName()); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/PathUpdateTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/PathUpdateTests.java new file mode 100644 index 000000000..3ec6d53c3 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/PathUpdateTests.java @@ -0,0 +1,271 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.joining; + +import static org.alfresco.elasticsearch.SearchQueryService.req; + +import java.util.Arrays; +import jakarta.json.Json; +import jakarta.json.JsonObject; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.elasticsearch.utility.ElasticsearchRESTHelper; +import org.alfresco.rest.core.RestWrapper; +import org.alfresco.rest.model.RestCategoryModel; +import org.alfresco.rest.model.RestNodeBodyMoveCopyModel; +import org.alfresco.rest.model.RestNodeModel; +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.model.ContentModel; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; +import org.alfresco.utility.report.log.Step; + +/** + * Tests to check that paths are updated correctly. + */ +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializer.class) +public class PathUpdateTests extends AbstractTestNGSpringContextTests +{ + @Autowired + private ServerHealth serverHealth; + @Autowired + private DataUser dataUser; + @Autowired + protected RestWrapper restClient; + @Autowired + private ElasticsearchRESTHelper helper; + @Autowired + protected SearchQueryService searchQueryService; + + private UserModel testUser; + private SiteModel testSite; + + /** + * Create a user and a private site containing some nested folders with a document in. + */ + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + serverHealth.isServerReachable(); + serverHealth.assertServerIsOnline(); + + Step.STEP("Create a test user and private site."); + testUser = dataUser.createRandomTestUser(); + testSite = helper.createPrivateSite(testUser); + } + + @Test + public void testChangeFileNameUpdatesPath() + { + Step.STEP("Create a file in the site."); + FileModel testFile = helper.createFileInSite(testUser, testSite); + + Step.STEP("Update the filename."); + RestNodeModel updatedFile = renameNode(testFile); + + Step.STEP("Check the path is updated"); + SearchRequest query = req("PATH:\"" + pathInSite(testSite, updatedFile.getName()) + "\""); + searchQueryService.expectResultsFromQuery(query, testUser, updatedFile.getName()); + } + + @Test + public void testChangeFileParentUpdatesPath() + { + Step.STEP("Create a file next to a folder."); + FileModel testFile = helper.createFileInSite(testUser, testSite); + FolderModel testFolder = helper.createFolderInSite(testUser, testSite); + + Step.STEP("Move the file into the folder and check the path is updated."); + moveNode(testFile, testFolder); + + Step.STEP("Check the path is updated"); + SearchRequest query = req("PATH:\"" + pathInSite(testSite, testFolder.getName(), testFile.getName()) + "\""); + searchQueryService.expectResultsFromQuery(query, testUser, testFile.getName()); + } + + @Test + public void testChangeFolderNameUpdatesPath() + { + Step.STEP("Create a file in a folder."); + FolderModel testFolder = helper.createFolderInSite(testUser, testSite); + FileModel testFile = helper.createFileInFolder(testUser, testFolder); + + Step.STEP("Update the folder's name."); + RestNodeModel updatedFolder = renameNode(testFolder); + + Step.STEP("Check the path is updated"); + SearchRequest query = req("PATH:\"" + pathInSite(testSite, updatedFolder.getName(), testFile.getName()) + "\""); + searchQueryService.expectResultsFromQuery(query, testUser, testFile.getName()); + } + + @Test + public void testChangeFolderParentUpdatesPath() + { + Step.STEP("Create two folders with a file in the first."); + FolderModel firstFolder = helper.createFolderInSite(testUser, testSite); + FolderModel secondFolder = helper.createFolderInSite(testUser, testSite); + FileModel testFile = helper.createFileInFolder(testUser, firstFolder); + + Step.STEP("Move the first folder into the second."); + moveNode(firstFolder, secondFolder); + + Step.STEP("Check the path is updated"); + SearchRequest query = req("PATH:\"" + pathInSite(testSite, secondFolder.getName(), firstFolder.getName(), testFile.getName()) + "\""); + searchQueryService.expectResultsFromQuery(query, testUser, testFile.getName()); + } + + @Test + public void testChangeCategoriesUpdatesPath() + { + Step.STEP("Create two categories and a file in the first."); + RestCategoryModel firstCategory = helper.createCategory(); + RestCategoryModel secondCategory = helper.createCategory(); + FileModel testFile = helper.createFileInSite(testUser, testSite); + helper.linkToCategory(testUser, testFile, firstCategory); + + Step.STEP("Remove the file from the first category and add it to the second."); + helper.unlinkFromCategory(testUser, testFile, firstCategory); + helper.linkToCategory(testUser, testFile, secondCategory); + + Step.STEP("Check there is no path for the first category."); + SearchRequest query = req("PATH:\"" + categoryPath(firstCategory.getName(), testFile.getName()) + "\""); + searchQueryService.expectNoResultsFromQuery(query, testUser); + + Step.STEP("Check there is a path for the second category."); + query = req("PATH:\"" + categoryPath(secondCategory.getName(), testFile.getName()) + "\""); + searchQueryService.expectResultsFromQuery(query, testUser, testFile.getName()); + } + + @Test + public void testChangeCategoryNameUpdatesPath() + { + Step.STEP("Create a category and a file in it."); + RestCategoryModel category = helper.createCategory(); + FileModel testFile = helper.createFileInSite(testUser, testSite); + helper.linkToCategory(testUser, testFile, category); + + Step.STEP("Update the name of the category."); + String newCategoryName = category.getName() + "_updated"; + restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI() + .usingCategory(category) + .updateCategory(RestCategoryModel.builder().name(newCategoryName).create()); + + Step.STEP("Check there is a path with the updated category name."); + SearchRequest query = req("PATH:\"" + categoryPath(newCategoryName, testFile.getName()) + "\""); + searchQueryService.expectResultsFromQuery(query, testUser, testFile.getName()); + } + + @Test + public void testChangeCategoryPathUpdatesPath() + { + Step.STEP("Create two nested categories and assign a file to the child."); + RestCategoryModel parentCategory = helper.createCategory(); + RestCategoryModel childCategory = helper.createCategory(parentCategory); + FileModel testFile = helper.createFileInSite(testUser, testSite); + helper.linkToCategory(testUser, testFile, parentCategory, childCategory); + + Step.STEP("Update the parent category name and check the file's paths are updated."); + String newCategoryName = parentCategory.getName() + "_updated"; + restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI() + .usingCategory(parentCategory) + .updateCategory(RestCategoryModel.builder().name(newCategoryName).create()); + + Step.STEP("Check there is a path with the updated category name."); + SearchRequest query = req("PATH:\"" + categoryPath(newCategoryName, childCategory.getName(), testFile.getName()) + "\""); + searchQueryService.expectResultsFromQuery(query, testUser, testFile.getName()); + } + + /** + * Rename the specified node to have "_updated" on the end. + * + * @param node + * The node to update. + * @return The updated node. + */ + private RestNodeModel renameNode(ContentModel node) + { + String newName = node.getName() + "_updated"; + JsonObject renameJson = Json.createObjectBuilder().add("properties", + Json.createObjectBuilder().add("cm:name", newName)).build(); + return restClient.authenticateUser(testUser).withCoreAPI().usingNode(node).updateNode(renameJson.toString()); + } + + /** + * Move the specified node to a folder. + * + * @param node + * The node to move. + * @param targetFolder + * The folder to move the node to. + * @return The updated node. + */ + private RestNodeModel moveNode(ContentModel node, FolderModel targetFolder) + { + RestNodeBodyMoveCopyModel moveBody = new RestNodeBodyMoveCopyModel(); + moveBody.setTargetParentId(targetFolder.getNodeRef()); + return restClient.authenticateUser(testUser).withCoreAPI().usingNode(node).move(moveBody); + } + + /** + * Create a path to a file or folder in a site. + * + * @param site + * The site object. + * @param documentLibraryNames + * The list of names of nodes from the document library to the target file or folder. + * @return An absolute path suitable for use in a path query. + */ + private String pathInSite(SiteModel site, String... documentLibraryNames) + { + return "/app:company_home/st:sites/cm:" + site.getId() + "/cm:documentLibrary/cm:" + stream(documentLibraryNames).collect(joining("/cm:")); + } + + /** + * Create a path to a file or folder via the category hierarchy. + * + * @param nodeNames + * The ordered list of node names from the root category to the node that was categorised. + * @return An absolute path through the categories to the specified node. + */ + private String categoryPath(String... nodeNames) + { + return "/cm:categoryRoot/cm:generalclassifiable/cm:" + Arrays.stream(nodeNames).collect(joining("/cm:")); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/SearchQueryService.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/SearchQueryService.java new file mode 100644 index 000000000..bfb940236 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/SearchQueryService.java @@ -0,0 +1,286 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import com.google.common.collect.Sets; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; + +import org.alfresco.rest.core.RestWrapper; +import org.alfresco.rest.search.RestRequestQueryModel; +import org.alfresco.rest.search.RestRequestTemplatesModel; +import org.alfresco.rest.search.SearchNodeModel; +import org.alfresco.rest.search.SearchRequest; +import org.alfresco.rest.search.SearchResponse; +import org.alfresco.utility.Utility; +import org.alfresco.utility.model.UserModel; + +/** A class providing methods for testing search queries. */ +public class SearchQueryService +{ + /** Maximum time to allow for search query to return correct results. */ + private static final int MAX_TIME = 10000; + @Autowired + private RestWrapper client; + + /** Assert that the query returns no results. */ + public void expectNoResultsFromQuery(SearchRequest searchRequest, UserModel testUser) + { + expectResultsFromQuery(searchRequest, testUser); + } + + /** Assert that the query returns something, without checking exactly what it returns. */ + public void expectSomeResultsFromQuery(SearchRequest searchRequest, UserModel testUser) + { + Consumer assertNotEmpty = searchResponse -> assertFalse(searchResponse.isEmpty()); + expectResultsFromQuery(searchRequest, testUser, assertNotEmpty); + } + + public void expectResultsInOrder(SearchRequest searchRequest, UserModel user, boolean isAscending, String... expected) + { + Consumer response = searchResponse -> assertNamesInOrder(searchResponse, isAscending, expected); + expectResultsFromQuery(searchRequest, user, response); + } + + public void expectResultsInOrder(SearchRequest searchRequest, UserModel user, String... expectedOrder) + { + Consumer response = searchResponse -> assertNamesInOrder(searchResponse, expectedOrder); + expectResultsFromQuery(searchRequest, user, response); + } + + public void expectResultsStartingWithOneOf(SearchRequest searchRequest, UserModel user, String... expected) + { + Consumer response = searchResponse -> { + List expectedFirstElements = List.of(expected); + String actualFirstElement = searchResponse.getEntries().stream() + .map(SearchNodeModel::getModel) + .map(SearchNodeModel::getName) + .findFirst() + .orElse(null); + assertTrue(expectedFirstElements.contains(actualFirstElement), "Unexpected search results - actual first element: " + actualFirstElement + ", expected one of: " + expectedFirstElements + " |"); + }; + expectResultsFromQuery(searchRequest, user, response); + } + + public void expectResultsFromQuery(SearchRequest searchRequest, UserModel user, String... expected) + { + Consumer assertNames = searchResponse -> assertNames(searchResponse, expected); + expectResultsFromQuery(searchRequest, user, assertNames); + } + + /** Check that the specified results are all included in the result set. */ + public void expectResultsInclude(SearchRequest searchRequest, UserModel user, String... expected) + { + Consumer assertNames = searchResponse -> assertNamesInclude(searchResponse, expected); + expectResultsFromQuery(searchRequest, user, assertNames); + } + + public void expectNodeTypesFromQuery(SearchRequest searchRequest, UserModel user, String... expected) + { + Consumer assertNodeTypes = searchResponse -> assertNodeTypes(searchResponse, expected); + expectResultsFromQuery(searchRequest, user, assertNodeTypes); + } + + public void expectNodeRefsFromQuery(SearchRequest searchRequest, UserModel user, String... expectedNodeRefs) + { + Consumer assertNames = searchResponse -> assertNodeRefs(searchResponse, expectedNodeRefs); + expectResultsFromQuery(searchRequest, user, assertNames); + } + + public void expectAllResultsFromQuery(SearchRequest searchRequest, UserModel user, Predicate assertionMethod) + { + Function failureMessage = searchNodeModel -> "'" + searchNodeModel.getName() + "' did not satisfy predicate."; + expectAllResultsFromQuery(searchRequest, user, assertionMethod, failureMessage); + } + + public void expectAllResultsFromQuery(SearchRequest searchRequest, UserModel user, Predicate assertionMethod, Function failureMessageFunction) + { + expectResultsFromQuery(searchRequest, user, searchResponse -> assertAllSearchResults(searchResponse, assertionMethod, failureMessageFunction)); + } + + public void expectTotalHitsFromQuery(SearchRequest searchRequest, UserModel user, int expected) + { + expectResultsFromQuery(searchRequest, user, searchResponse -> assertTotalHitsResults(searchResponse, expected)); + } + + private void expectResultsFromQuery(SearchRequest searchRequest, UserModel user, Consumer assertionMethod) + { + try + { + Utility.sleep(1000, MAX_TIME, () -> { + SearchResponse response = client.authenticateUser(user) + .withSearchAPI() + .search(searchRequest); + client.assertStatusCodeIs(HttpStatus.OK); + assertionMethod.accept(response); + }); + } + catch (InterruptedException e) + { + fail("InterruptedException received while waiting for results."); + } + } + + public void expectErrorFromQuery(SearchRequest searchRequest, org.alfresco.utility.model.UserModel user, + HttpStatus expectedStatusCode, String containsErrorString) + { + client.authenticateUser(user).withSearchAPI().search(searchRequest); + client.assertStatusCodeIs(expectedStatusCode); + client.assertLastError().containsSummary(containsErrorString); + } + + private void assertNodeRefs(SearchResponse actual, String... expected) + { + Set result = actual.getEntries().stream() + .map(SearchNodeModel::getModel) + .map(SearchNodeModel::getId) + .collect(Collectors.toSet()); + Set expectedList = Sets.newHashSet(expected); + assertEquals(result, expectedList, "Unexpected search results - got " + result + " expected " + expectedList); + } + + private void assertNames(SearchResponse actual, String... expected) + { + Set result = actual.getEntries().stream() + .map(SearchNodeModel::getModel) + .map(SearchNodeModel::getName) + .collect(Collectors.toSet()); + Set expectedList = Sets.newHashSet(expected); + assertEquals(result, expectedList, "Unexpected search results - got " + result + " expected " + expectedList); + } + + /** Check that the given names are included in the result set. */ + private void assertNamesInclude(SearchResponse actual, String... expected) + { + Set expectedSet = Sets.newHashSet(expected); + // Filter to only include results that were expected. + Set filteredResults = actual.getEntries().stream() + .map(SearchNodeModel::getModel) + .map(SearchNodeModel::getName) + .filter(name -> expectedSet.contains(name)) + .collect(Collectors.toSet()); + assertEquals(filteredResults, expectedSet, "Did not receive all the expected search results - got " + filteredResults + " from expected set " + expectedSet); + } + + private List orderNames(boolean isAscending, String... filename) + { + List orderedNames = Arrays.stream(filename).sorted().collect(Collectors.toList()); + if (!isAscending) + { + orderedNames = orderedNames.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList()); + } + return orderedNames; + } + + private void assertNamesInOrder(SearchResponse actual, boolean isAscending, String... expected) + { + List expectedInOrder = orderNames(isAscending, expected); + List result = actual.getEntries().stream() + .map(SearchNodeModel::getModel) + .map(SearchNodeModel::getName) + .collect(Collectors.toList()); + assertEquals(result, expectedInOrder, "Unexpected search results - got " + result + " expected " + expectedInOrder); + } + + private void assertNamesInOrder(SearchResponse actual, String... expectedOrder) + { + List expectedInOrder = List.of(expectedOrder); + List result = actual.getEntries().stream() + .map(SearchNodeModel::getModel) + .map(SearchNodeModel::getName) + .toList(); + assertEquals(result, expectedInOrder, "Unexpected search results - got " + result + " expected " + expectedInOrder); + } + + private void assertNodeTypes(SearchResponse actual, String... expected) + { + Set result = actual.getEntries().stream() + .map(SearchNodeModel::getModel) + .map(SearchNodeModel::getNodeType) + .collect(Collectors.toSet()); + Set expectedList = Sets.newHashSet(expected); + assertEquals(result, expectedList, "Unexpected search results - got " + result + " expected " + expectedList); + } + + private void assertAllSearchResults(SearchResponse actual, Predicate assertion, Function failureMessageFunction) + { + String result = actual.getEntries().stream() + .map(SearchNodeModel::getModel) + .filter(Predicate.not(assertion)) + .map(failureMessageFunction) + .collect(Collectors.joining("\n")); + assertTrue(result.isEmpty(), "assertAllSearchResults failed with these issues:\n" + result); + } + + private void assertTotalHitsResults(SearchResponse actual, int expected) + { + int totalItems = actual.getPagination().getTotalItems(); + assertEquals(totalItems, expected, "Unexpected totalItems results - got " + totalItems + " expected " + expected); + } + + public static SearchRequest req(String query) + { + return req(null, query); + } + + public static SearchRequest req(String language, String query) + { + RestRequestQueryModel restRequestQueryModel = new RestRequestQueryModel(); + restRequestQueryModel.setQuery(query); + Optional.ofNullable(language).ifPresent(restRequestQueryModel::setLanguage); + return new SearchRequest(restRequestQueryModel); + } + + public static SearchRequest req(String language, String query, Map templates) + { + RestRequestQueryModel restRequestQueryModel = new RestRequestQueryModel(); + restRequestQueryModel.setQuery(query); + Optional.ofNullable(language).ifPresent(restRequestQueryModel::setLanguage); + SearchRequest request = new SearchRequest(restRequestQueryModel); + List templatesModels = templates.entrySet().stream() + .map(entry -> RestRequestTemplatesModel.builder().name(entry.getKey()).template(entry.getValue()).create()) + .toList(); + request.setTemplates(templatesModels); + + return request; + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/basicAuth/AlfrescoStackInitializerESBasicAuth.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/basicAuth/AlfrescoStackInitializerESBasicAuth.java new file mode 100644 index 000000000..2093f98c6 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/basicAuth/AlfrescoStackInitializerESBasicAuth.java @@ -0,0 +1,178 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch.basicAuth; + +import java.io.IOException; + +import org.testcontainers.containers.GenericContainer; + +import org.alfresco.tas.AlfrescoStackInitializer; +import org.alfresco.tas.SearchEngineType; + +/** + * ACS Stack Docker Compose initializer with Basic Authentication for Search Engine service (Opensearch or Elasticsearch). + */ +public class AlfrescoStackInitializerESBasicAuth extends AlfrescoStackInitializer +{ + + // Default Elasticsearch credentials + private static final String SEARCH_ENGINE_USERNAME = "elastic"; + private static final String SEARCH_ENGINE_PASSWORD = "bob123"; // pragma: allowlist secret + private static final String OPENSEARCH_TEST_ROLE = "test_role"; + + @Override + public void configureSecuritySettings(GenericContainer searchEngineContainer) + { + SearchEngineType usedEngine = getImagesConfig().getSearchEngineType(); + + if (SearchEngineType.OPENSEARCH_ENGINE.equals(usedEngine)) + { + configureNewUser(searchEngineContainer); + } + } + + private void configureNewUser(GenericContainer opensearchContainer) + { + try + { + // Using password hash for setting up test only + String passwordHash = hashPassword(opensearchContainer, SEARCH_ENGINE_PASSWORD); + + addNewUser(opensearchContainer, SEARCH_ENGINE_USERNAME, passwordHash); + addNewRole(opensearchContainer, OPENSEARCH_TEST_ROLE, CUSTOM_ALFRESCO_INDEX); + addNewRoleMapping(opensearchContainer, OPENSEARCH_TEST_ROLE, SEARCH_ENGINE_USERNAME); + applyNewSecurityConfigs(opensearchContainer); + } + catch (IOException | InterruptedException e) + { + e.printStackTrace(); + } + } + + private String hashPassword(GenericContainer opensearchContainer, String password) throws IOException, InterruptedException + { + return opensearchContainer.execInContainer("/usr/share/opensearch/plugins/opensearch-security/tools/hash.sh", "-p", password) + .getStdout().strip(); + } + + private void applyNewSecurityConfigs(GenericContainer opensearchContainer) throws IOException, InterruptedException + { + opensearchContainer.execInContainer("sh", "-c", "/usr/share/opensearch/plugins/opensearch-security/tools/securityadmin.sh " + + "-cd /usr/share/opensearch/config/opensearch-security " + + "-icl -nhnv " + + "-cert /usr/share/opensearch/config/kirk.pem " + + "-cacert /usr/share/opensearch/config/root-ca.pem " + + "-key /usr/share/opensearch/config/kirk-key.pem"); + } + + private void addNewRoleMapping(GenericContainer opensearchContainer, String role, String username) throws IOException, InterruptedException + { + opensearchContainer.execInContainer( + "sh", "-c", "echo '\n\n" + newOpensearchRoleMapping(role, username) + "' >> /usr/share/opensearch/config/opensearch-security/roles_mapping.yml"); + } + + private void addNewRole(GenericContainer opensearchContainer, String role, String index) throws IOException, InterruptedException + { + opensearchContainer.execInContainer( + "sh", "-c", "echo '\n\n" + newOpensearchRule(role, index) + "' >> /usr/share/opensearch/config/opensearch-security/roles.yml"); + } + + private void addNewUser(GenericContainer opensearchContainer, String username, String passwordHash) throws IOException, InterruptedException + { + opensearchContainer.execInContainer( + "sh", "-c", "echo '\n\n" + newOpensearchUser(username, passwordHash) + "' >> /usr/share/opensearch/config/opensearch-security/internal_users.yml"); + } + + private String newOpensearchUser(String username, String passwordHash) + { + return username + ":\n" + + " hash: \"" + passwordHash + "\"\n" + + " reserved: false\n" + + " backend_roles:\n" + + " - \"all_access\"\n" + + " description: \"New user for testing purposes\""; + } + + private String newOpensearchRule(String role, String indexName) + { + return role + ":\n" + + " cluster_permissions:\n" + + " - cluster_all\n" + + " index_permissions:\n" + + " - index_patterns:\n" + + " - \"" + indexName + "\"\n" + + " - \"alfresco-reindex-state\"\n" + + " allowed_actions:\n" + + " - \"*\"\n"; + } + + private String newOpensearchRoleMapping(String role, String user) + { + return role + ":\n" + + " reserved: true\n" + + " users:\n" + + " - \"" + user + "\""; + } + + @Override + protected GenericContainer createBatchIndexingContainer() + { + GenericContainer container = super.createBatchIndexingContainer(); + container.withEnv("SPRING_ELASTICSEARCH_REST_USERNAME", SEARCH_ENGINE_USERNAME); + container.withEnv("SPRING_ELASTICSEARCH_REST_PASSWORD", SEARCH_ENGINE_PASSWORD); + return container; + } + + @Override + protected GenericContainer createSearchEngineContainer() + { + SearchEngineType usedEngine = getImagesConfig().getSearchEngineType(); + + if (SearchEngineType.OPENSEARCH_ENGINE.equals(usedEngine)) + { + return super.createOpensearchContainer() + .withEnv("plugins.security.disabled", "false") + .withEnv("plugins.security.ssl.http.enabled", "false"); + } + else + { + return super.createElasticContainer() + .withEnv("xpack.security.enabled", "true") + .withEnv("ELASTIC_PASSWORD", SEARCH_ENGINE_PASSWORD); + } + } + + @Override + protected GenericContainer createAlfrescoContainer() + { + GenericContainer container = super.createAlfrescoContainer(); + String javaOpts = (String) container.getEnvMap().get("JAVA_OPTS"); + javaOpts = javaOpts + " -Delasticsearch.user=" + SEARCH_ENGINE_USERNAME + " " + + "-Delasticsearch.password=" + SEARCH_ENGINE_PASSWORD; + container.getEnvMap().put("JAVA_OPTS", javaOpts); + return container; + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/basicAuth/ElasticsearchBasicAuthTests.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/basicAuth/ElasticsearchBasicAuthTests.java new file mode 100644 index 000000000..a137d8707 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/basicAuth/ElasticsearchBasicAuthTests.java @@ -0,0 +1,91 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch.basicAuth; + +import static org.alfresco.elasticsearch.SearchQueryService.req; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.elasticsearch.SearchQueryService; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; + +/** + * Basic test for Elasticsearch server with Basic Authentication. The aim of this class is to test that Basic Authentication is working as expected. + */ +@ContextConfiguration(locations = "classpath:alfresco-elasticsearch-context.xml", + initializers = AlfrescoStackInitializerESBasicAuth.class) +public class ElasticsearchBasicAuthTests extends AbstractTestNGSpringContextTests +{ + private static final String FILE_0_NAME = "test.txt"; + + @Autowired + private ServerHealth serverHealth; + @Autowired + private DataUser dataUser; + @Autowired + private DataContent dataContent; + @Autowired + private DataSite dataSite; + @Autowired + private SearchQueryService searchQueryService; + + private UserModel userSite1; + private SiteModel siteModel1; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + serverHealth.assertServerIsOnline(); + userSite1 = dataUser.createRandomTestUser(); + siteModel1 = dataSite.usingUser(userSite1).createPrivateRandomSite(); + createContent(FILE_0_NAME, "This is the first test", siteModel1, userSite1); + } + + private FileModel createContent(String filename, String content, SiteModel site, UserModel user) + { + FileModel fileModel = new FileModel(filename, FileType.TEXT_PLAIN, content); + return dataContent.usingUser(user).usingSite(site) + .createContent(fileModel); + } + + @Test(groups = TestGroup.SEARCH) + public void searchCanFindAFile() + { + searchQueryService.expectResultsFromQuery(req("first AND SITE:\"" + siteModel1.getId() + "\""), userSite1, FILE_0_NAME); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/retry/RetryAnalyzer.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/retry/RetryAnalyzer.java new file mode 100644 index 000000000..f4045ad18 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/retry/RetryAnalyzer.java @@ -0,0 +1,63 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch.retry; + +import java.util.ConcurrentModificationException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.IRetryAnalyzer; +import org.testng.ITestResult; + +public class RetryAnalyzer implements IRetryAnalyzer +{ + private static final Logger LOGGER = LoggerFactory.getLogger(RetryAnalyzer.class); + private static final int RETRY_LIMIT = 3; + private int retryNumber = 0; + + @Override + public boolean retry(ITestResult testResult) + { + retryNumber++; + Throwable throwable = testResult.getThrowable(); + if (retryNumber == RETRY_LIMIT) + { + LOGGER.info("Retry limit reached: {}, shouldRetry: {}", retryNumber, false, throwable); + return false; + } + else + { + boolean shouldRetry = throwable != null + && (throwable instanceof IllegalStateException && throwable.getMessage() != null + && throwable.getMessage().contains("connection still allocated")) + || (throwable instanceof ConcurrentModificationException) + || (throwable instanceof AssertionError && throwable.getMessage() != null + && throwable.getMessage().contains("Maximum retry period reached")); + LOGGER.info("Retry: {}, shouldRetry: {}", retryNumber, shouldRetry, throwable); + return shouldRetry; + } + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/retry/RetryAnnotationTransformer.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/retry/RetryAnnotationTransformer.java new file mode 100644 index 000000000..9cc722617 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/retry/RetryAnnotationTransformer.java @@ -0,0 +1,44 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch.retry; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +import org.testng.IAnnotationTransformer; +import org.testng.annotations.ITestAnnotation; + +/** + * Add the {@link RetryAnalyzer} to each test. + */ +public class RetryAnnotationTransformer implements IAnnotationTransformer +{ + @Override + public void transform(ITestAnnotation annotation, Class testClass, Constructor testConstructor, Method testMethod) + { + annotation.setRetryAnalyzer(RetryAnalyzer.class); + } +} diff --git a/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/utility/ElasticsearchRESTHelper.java b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/utility/ElasticsearchRESTHelper.java new file mode 100644 index 000000000..f99ac53cd --- /dev/null +++ b/tests/tas-elasticsearch/src/test/java/org/alfresco/elasticsearch/utility/ElasticsearchRESTHelper.java @@ -0,0 +1,166 @@ +/* + * #%L + * Alfresco Tas Elasticsearch + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.elasticsearch.utility; + +import static org.alfresco.utility.model.FileType.TEXT_PLAIN; + +import org.springframework.beans.factory.annotation.Autowired; + +import org.alfresco.rest.core.RestWrapper; +import org.alfresco.rest.model.RestCategoryLinkBodyModel; +import org.alfresco.rest.model.RestCategoryModel; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.data.DataUser; +import org.alfresco.utility.data.RandomData; +import org.alfresco.utility.model.ContentModel; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.UserModel; + +/** Helper methods for Elasticsearch E2E tests. */ +public class ElasticsearchRESTHelper +{ + /** The alias for the root of the category hierarchy. */ + public static final String ROOT_CATEGORY_ALIAS = "-root-"; + /** The root of the category hierarchy. */ + private static final RestCategoryModel ROOT_CATEGORY = RestCategoryModel.builder().id(ROOT_CATEGORY_ALIAS).name(ROOT_CATEGORY_ALIAS).create(); + + @Autowired + private RestWrapper client; + @Autowired + private DataSite dataSite; + @Autowired + private DataContent dataContent; + @Autowired + private DataUser dataUser; + + /** + * Create a private site. + * + * @param user + * The user to use. + * @return The new site. + */ + public SiteModel createPrivateSite(UserModel user) + { + return dataSite.usingUser(user).createPrivateRandomSite(); + } + + /** + * Create a folder in a site. + * + * @param user + * The user to use. + * @param site + * The site to create the folder in. + * @return The new folder. + */ + public FolderModel createFolderInSite(UserModel user, SiteModel site) + { + return dataContent.usingUser(user).usingSite(site).createFolder(); + } + + /** + * Create a text file in a site. + * + * @param user + * The user to use. + * @param site + * The site to create the file in. + * @return The new file. + */ + public FileModel createFileInSite(UserModel user, SiteModel site) + { + FileModel fileModel = FileModel.getRandomFileModel(TEXT_PLAIN); + return dataContent.usingUser(user).usingSite(site).createContent(fileModel); + } + + /** + * Create a file in a folder. + * + * @param user + * The user to use. + * @param folder + * The folder to create the file in. + * @return The new file. + */ + public FileModel createFileInFolder(UserModel user, FolderModel folder) + { + FileModel fileModel = FileModel.getRandomFileModel(TEXT_PLAIN); + return dataContent.usingUser(user).usingResource(folder).createContent(fileModel); + } + + /** + * Create a category. + * + * @param ancestorCategories + * The path of categories between the root and the new category, or leave blank to create the category at the "-root-". + * @return The newly created category. + */ + public RestCategoryModel createCategory(RestCategoryModel... ancestorCategories) + { + RestCategoryModel parent = (ancestorCategories.length > 0 ? ancestorCategories[ancestorCategories.length - 1] : ROOT_CATEGORY); + return client.authenticateUser(dataUser.getAdminUser()).withCoreAPI() + .usingCategory(parent) + .createSingleCategory(RestCategoryModel.builder().name(RandomData.getRandomAlphanumeric()).create()); + } + + /** + * Link a file or folder to a category. + * + * @param user + * The user who should create the link. + * @param node + * The file or folder to be linked. + * @param categoryHierarchy + * The full list of categories from the root (excluding "-root-") to the category to use. + * @return The category that was linked to. + */ + public RestCategoryModel linkToCategory(UserModel user, ContentModel node, RestCategoryModel... categoryHierarchy) + { + RestCategoryModel linkedToCategory = (categoryHierarchy.length > 0 ? categoryHierarchy[categoryHierarchy.length - 1] : ROOT_CATEGORY); + return client.authenticateUser(user).withCoreAPI().usingNode(node) + .linkToCategory(RestCategoryLinkBodyModel.builder().categoryId(linkedToCategory.getId()).create()); + } + + /** + * Unlink a node from a category. + * + * @param user + * The user who should remove the link. + * @param node + * The node to unlink. + * @param categoryHierarchy + * The full list of categories from the root (excluding "-root-") to the category to use. + */ + public void unlinkFromCategory(UserModel user, ContentModel node, RestCategoryModel... categoryHierarchy) + { + RestCategoryModel linkedToCategory = (categoryHierarchy.length > 0 ? categoryHierarchy[categoryHierarchy.length - 1] : ROOT_CATEGORY); + client.authenticateUser(user).withCoreAPI().usingNode(node).unlinkFromCategory(linkedToCategory.getId()); + } +} diff --git a/tests/tas-elasticsearch/src/test/resources/alfresco-elasticsearch-context.xml b/tests/tas-elasticsearch/src/test/resources/alfresco-elasticsearch-context.xml new file mode 100644 index 000000000..33e893552 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/resources/alfresco-elasticsearch-context.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/tests/tas-elasticsearch/src/test/resources/exactTermSearch.properties b/tests/tas-elasticsearch/src/test/resources/exactTermSearch.properties new file mode 100644 index 000000000..255b677d6 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/resources/exactTermSearch.properties @@ -0,0 +1,5 @@ +# Exact Term search is switched off by default as it introduces index size overhead. +#to enable it, please uncomment the following on a datatype or property name basis +alfresco.cross.locale.datatype.0={http://www.alfresco.org/model/dictionary/1.0}content +alfresco.cross.locale.property.0={http://www.alfresco.org/model/content/1.0}content +alfresco.cross.locale.property.1={http://www.alfresco.org/model/content/1.0}name \ No newline at end of file diff --git a/tests/tas-elasticsearch/src/test/resources/log4j2.properties b/tests/tas-elasticsearch/src/test/resources/log4j2.properties new file mode 100644 index 000000000..9276582cd --- /dev/null +++ b/tests/tas-elasticsearch/src/test/resources/log4j2.properties @@ -0,0 +1,16 @@ +# Root logger option +rootLogger.level=info +rootLogger.appenderRef.stdout.ref=ConsoleAppender + +###### Console appender definition ####### +appender.console.type=Console +appender.console.name=ConsoleAppender +appender.console.layout.type=PatternLayout +appender.console.layout.pattern=[%t] %d{HH:mm:ss} %-5p %c{1}:%L - %replace{%m}{[\r\n]+}{}%n + +logger.alfresco-elasticsearch.name=org.alfresco.elasticsearch +logger.alfresco-elasticsearch.level=info +logger.alfresco-rest-core.name=org.alfresco.rest.core +logger.alfresco-rest-core.level=warn +logger.alfresco-utility-report-log.name=org.alfresco.utility.report.log +logger.alfresco-utility-report-log.level=info \ No newline at end of file diff --git a/tests/tas-elasticsearch/src/test/resources/test-data/alfresco-logo.png b/tests/tas-elasticsearch/src/test/resources/test-data/alfresco-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5c3403f00ebf3e0e7699bd06754fb1ca596892ec GIT binary patch literal 6059 zcmV;c7gXqpP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000cuNkl+-Gi*o7|fx36KB@fdniFL|&pUP(+ZaU`NzCgKdYG zqK@F|*v?pGXs6UiK}59LDzs_`+d<_qwGu!WPz*sNgakqo@_;;ULLMY1_jS&mKN2J% z_mwGs%x|r`vTn}K-rw)FDSnFE0;8Q4lu}{H?%NpC{7M z@Qe_mJ(9gOO_L=hC5#$13aizMs;X2~RgssMcVh@hDW3#h*>TFHtS_jcvC|uL?d+_0 zem-{uBb?R>;BFzr`EdK6I(16s<>j$!*De5BT3T4TbSWhzB`7xl{0dFVjZc1ZURnL| z`Hj*#uV@IKo2!Y*hkDP#%a*CAlMJeSW zDP@(E@}iWo9N6;Vsb+SSUXGY_msjJ_9cTIaQfE4FT1r{byI+}<^1q~%k4Y&LuC7;A z6)7bEd3kvh78auGI;yIMcOfa|O~5AL_L9af4pz2P*XF^k=_J`qBwG{~j7!97HuB2h zI^O=O0j>A2aFT9S*#Fqg#MbUYUMhsB1cYN0O2+L7tHa%U?hC4_sudL#3>!9#`1p8S zE*A|A4dmqHgeD-RTnfDTd1bqKT|qUc8@dLp(QBKKUOzXFN#UL4ley6DVd;*uba=Gj z`?|fz=60mp%YCy(@#cK+ee%e)1wz4)^IZ&+A4k}dPXpsrRi&t?h`6{oy1KfUG-(oM zvpF~cDdnRQe)qG#Uf|7R^?}!Tedsk!-+nh*Ol*5%8p%!@i?)}b`t*T+)82{hx|V!m z?NL;1o{x9?tw?ukzqP^|kMXWeD5;aGfRTYPC?VYDjZokLaQ6>bzsJWvY&?SI@w0yJ zh=KcdboB!daHXRgk55B5vjbSF4d347$mY@!y){G#A?W)F7R0aaJKt&!2_C>dP{W!} zby3d*JgA0Dn~~npChQ206oxuizQbeRF{Cg|&c`ax!q+x_veO=hWKX zs`6SvB`c<;U5k~=S0G4m8x#Rj4<0k^7tqdq46r5=7g}7}q;l4!hDT~L1by`Gfhy*X zc3?6HkP_M19eAM-j7+wH+8pvIy?i%7zLavPl#;6-rSF&CTzkv^T!^T`BdnI7&n`B0 zU^Oa?O|~Jsy+O5~;Iv^CW0&p^L)47{CT3X+cOu~NUvZ48u|bP4mvIGH~tf!q`u$uTAbf)>?BnM>ub=UVyX zObc$mj!3X0Raek+6(K+i?B7w*7GAh-4BDqFB2$>I4N!U@0@gLNhN5XYTM8=Kva6Jq zD{gdM;*7O2ZFB<9&duV5J2Uykfg1jBq7f3}&|BIE73jjC@b2nq7;6gf)gA5mU91<9 z)t;Y59sCM(nvl&=$Tlk?ItjoZjDQ*VQj>Dn>xXN3>HQPP>oiy2&_PLg6L0S=BY)fw z_O6}IZ6o7&{G;=zx{hqU5*c_!VdJuVW{*w8v+a3AtQV2nf^fJ4ug_{jwx)r>0`N=V zoDibmdUG7_@Msesc(0UKzNkkUlu$heUlm_s*s5LJJ2rv$9~?t;YCOUb8xiQKDz%|;|#M45IeDEMz((V9<-dV#MScG`hmQp%Zq32gT3a{SUAXE|Ke;|hw?9u`b@ zmxm$CK4wN{9Pcd&PeK?JHm|&eho%n2yZu&3C`Y-;1xlFa&uhjoViQI6f8s#ZX3n@i zjRJN^DIE&n1XjIv^b$w;b{Qv1%$y^3!|3Cs(87 zwnWg!3q;f}Y4WvF;`$2!`zklm?x`OF{Hp@Ie8rlD zjLbMbcxWtf*O7>^nfUGM=`5a{HaPIQKGWy=tE7_RL`imlA@tox^J%2~XeZN#+u8B`T;fvWd%#-^>{*@9!ilMa1K&9WL+r61U;a(8uTBY9mIdkcdx z+{P2rVpudTktxGsnV1$cn3`n)D60Q6U)F64?t;1(zwTwQVgwq~?3SRDft(~88}1pw z#55ECUHM1;T=5le6u08hd;!--n~cmzO=Wo8P0Sqr!=A>jqnJrU7IUGakw2Dw5~Ssj zm=zPTCP|W2C(f*g1s`Kh!U&w4o?%b_eSjFaZ(Jgq?j6YorJLALv=6ryI$v81lB`%C z(WY!>4*wxvUHpLj^ap9|I>#4v?{a_c%QSblv-A9+Kt2k9%MF!R48)st#>V@JH3dG? zVvHddzezJW7qSU%xpfFLBUNt;*UOP87P4j-ve1FezmoP{C`oeJ=Nti{mq z#EffRl;N=Q>cT93`uU1MfuEE*n+2nvW5cnv6x5!&c6Ii9DQY%R?D~d3oms>33D0t0 z)~pC-QBZu@M2owB3O6birVf1s;B^J)2G%@2J&oxZJ;i7JU775zc#D$Dje`O|FZ(BK zIJTApwIu_Kon!!sB{{Rh4yGhf+-k(%$CKuad>XqY5G-(mBKUsKl^;#MXcEE)Ijq{ifQ0QU+ZzP$cEpaD+;D^+ieX;!KS zms_FLqu|yA0x%gQ@n%ViRl}%2yfuw`M?cTzqURz4-z=ipS@LugDyaR38z|3Of$8yxBTaPQM-+?Nq4d+dogxP3KnAw9<7Tjo<{0AirN0n z@A$i%^~70+7{gsDL4<#85N}N<%Q=%}pMDP+!qotxjDg#aPM@G&HIQc0Xm!_dwDz6Q zml|P+Xtb!snv=p3$a2o$;+0BjuLSm6lOjT-f~7| zkQ|*Ia>Hn|lg7^Av(xnGo-9_;dW0Br9NBS!XP>(4Nt5AZY~mb%A|XU|NCHBL=K=rK zMl6&+`eoo$8*kQ0wDttQ>T9F4`7>5bcp_-N4`%>J)G%_B=K{PK(RdwrHZwk-Nki@r z@tUZGBwJEI-yiSv_p7jntKVW|d=87U?+Dn!SwdXuO^THT+5d(?7!LqDA|fD!cnLUq zTjt83BwT7G)6?hlpNRFg6;X!%fK*>AyU)GAqvM}uan`JU@epU$gT{x|XlGIG29lz) zE&vY;A!OhlHuzhb!@$hyw&NVCc@tMx#kbFrY$JK(J8$#-J>TWB+e5ldBg?K~3Zc!6 z_{l8DevV>SAzM#w;bK=8x$%C6Mr+q8P+?s1JZ2300XCE4G%#NXQA5xsJT!^5z<&V7 zrmk`>v>m6z(|{&@Oda|NWi2N;SoKm!;C&_5oXqW+tH@5cm9v*m(%xN;>T}`ocVRQd zlVHnWq+=RZqg@3y0_%kk-NDmq!p-gH}cH?9RPs~kyRXy5RU)=002ovPDHLkV1o9KgIWLp literal 0 HcmV?d00001 diff --git a/tests/tas-elasticsearch/src/test/resources/test-suites/elasticsearch-basic-auth-suite.xml b/tests/tas-elasticsearch/src/test/resources/test-suites/elasticsearch-basic-auth-suite.xml new file mode 100644 index 000000000..6bc7724e0 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/resources/test-suites/elasticsearch-basic-auth-suite.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/tests/tas-elasticsearch/src/test/resources/test-suites/elasticsearch-suite.xml b/tests/tas-elasticsearch/src/test/resources/test-suites/elasticsearch-suite.xml new file mode 100644 index 000000000..6dccdb1bc --- /dev/null +++ b/tests/tas-elasticsearch/src/test/resources/test-suites/elasticsearch-suite.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/tests/tas-elasticsearch/src/test/resources/test-suites/standard-elasticsearch-suites.xml b/tests/tas-elasticsearch/src/test/resources/test-suites/standard-elasticsearch-suites.xml new file mode 100644 index 000000000..9c5f5e5fa --- /dev/null +++ b/tests/tas-elasticsearch/src/test/resources/test-suites/standard-elasticsearch-suites.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/tests/tas-elasticsearch/src/test/resources/testcontainers.properties b/tests/tas-elasticsearch/src/test/resources/testcontainers.properties new file mode 100644 index 000000000..ee15ff401 --- /dev/null +++ b/tests/tas-elasticsearch/src/test/resources/testcontainers.properties @@ -0,0 +1,2 @@ +# By default this property is set to 30 seconds +pull.pause.timeout = 120 \ No newline at end of file diff --git a/tests/testcontainers-env/pom.xml b/tests/testcontainers-env/pom.xml new file mode 100644 index 000000000..d3e21fe10 --- /dev/null +++ b/tests/testcontainers-env/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + org.alfresco.tas + content-repository-community-testcontainers + Testcontainers environment + jar + + + org.alfresco + content-repository-community-tests + 26.2.0-A.24-SNAPSHOT + + + + 2.0.3 + + + + + junit + junit + test + + + org.alfresco.tas + restapi + + + org.springframework + spring-test + + + org.testcontainers + testcontainers-postgresql + ${dependency.testcontainers.version} + + + org.testcontainers + testcontainers-mysql + ${dependency.testcontainers.version} + + + org.testcontainers + testcontainers-mariadb + ${dependency.testcontainers.version} + + + org.testcontainers + testcontainers-elasticsearch + ${dependency.testcontainers.version} + + + org.testcontainers + testcontainers-mssqlserver + ${dependency.testcontainers.version} + + + + + + + src/main/resources + true + + + + diff --git a/tests/testcontainers-env/src/main/java/org/alfresco/tas/AlfrescoStackInitializer.java b/tests/testcontainers-env/src/main/java/org/alfresco/tas/AlfrescoStackInitializer.java new file mode 100644 index 000000000..fa4f3e714 --- /dev/null +++ b/tests/testcontainers-env/src/main/java/org/alfresco/tas/AlfrescoStackInitializer.java @@ -0,0 +1,521 @@ +/* + * #%L + * Alfresco Testcontainers Environment + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.tas; + +import static org.alfresco.tas.SystemPropertyHelper.getSystemProperty; + +import java.net.URI; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.Map; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.support.TestPropertySourceUtils; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.lifecycle.Startable; +import org.testcontainers.lifecycle.Startables; +import org.testng.Assert; +import org.testng.util.Strings; + +import org.alfresco.utility.report.log.Step; + +public class AlfrescoStackInitializer implements ApplicationContextInitializer +{ + private static final Logger LOGGER = LoggerFactory.getLogger(AlfrescoStackInitializer.class); + private static final Slf4jLogConsumer LOG_CONSUMER = new Slf4jLogConsumer(LOGGER); + + public static final String CUSTOM_ALFRESCO_INDEX = "custom-alfresco-index"; + + public static Network network; + + public static GenericContainer alfresco; + + public static GenericContainer searchEngineContainer; + + /** To create the kibana container for a test run then pass -Dkibana=true as an argument to the mvn command. */ + public static GenericContainer dashboardsContainer; + + /** Community Elasticsearch based indexer. */ + public static GenericContainer batchIndexer; + + @Override + public void initialize(ConfigurableApplicationContext configurableApplicationContext) + { + + // Wait till existing containers are stopped + if (alfresco != null) + { + if (alfresco.getDockerClient().listContainersCmd().withShowAll(true).exec().size() > 0) + { + try + { + LOGGER.info("Waiting for living containers to be stopped..."); + Thread.sleep(10000); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + } + } + + network = Network.newNetwork(); + + alfresco = createAlfrescoContainer(); + + JdbcDatabaseContainer database = createDatabaseContainer(); + + GenericContainer transformCore = createTransformCoreContainer(); + + GenericContainer activemq = createAMQContainer(); + + searchEngineContainer = createSearchEngineContainer(); + + startOrFail(searchEngineContainer); + + configureSecuritySettings(searchEngineContainer); + + startOrFail(database); + + startOrFail(activemq); + + startOrFail(transformCore); + + // We don't want Kibana to run on our CI, but it can be useful when investigating issues locally. + if (getSystemProperty("kibana", "false").equals("true")) + { + dashboardsContainer = createDashboardsContainer(); + startOrFail(dashboardsContainer); + } + + startOrFail(alfresco); + + alfresco.followOutput(LOG_CONSUMER); + + batchIndexer = createBatchIndexingContainer(); + + startOrFail(batchIndexer); + + TestPropertySourceUtils.addInlinedPropertiesToEnvironment(configurableApplicationContext, + "alfresco.server=" + alfresco.getContainerIpAddress(), + "alfresco.port=" + alfresco.getFirstMappedPort()); + + } + + private JdbcDatabaseContainer createDatabaseContainer() + { + switch (getImagesConfig().getDatabaseType()) + { + case POSTGRESQL_DB: + return createPosgresContainer(); + case MYSQL_DB: + return createMySqlContainer(); + case MARIA_DB: + return createMariaDBContainer(); + default: + throw new IllegalArgumentException("Database not set."); + } + } + + public void configureSecuritySettings(GenericContainer searchEngineContainer) + { + // empty for default execution + } + + private void startOrFail(Startable... startables) + { + try + { + Startables.deepStart(startables).get(); + } + catch (Exception e) + { + Assert.fail("Unable to start containers", e); + } + + } + + protected GenericContainer createBatchIndexingContainer() + { + DatabaseType databaseType = getImagesConfig().getDatabaseType(); + String driverJarPath = resolveDriverJarPath(databaseType.getDriver()); + String driverJarName = Paths.get(driverJarPath).getFileName().toString(); + + return new GenericContainer<>(getImagesConfig().getBatchIndexingImage()) + .withNetwork(network) + .withNetworkAliases("batch-indexer") + .withEnv("JAVA_OPTS", "-Xms512m -Xmx1536m -agentlib:jdwp=transport=dt_socket,address=*:5005,server=y,suspend=n") + .withEnv("SPRING_DATASOURCE_URL", databaseType.getUrl()) + .withEnv("SPRING_DATASOURCE_USERNAME", databaseType.getUsername()) + .withEnv("SPRING_DATASOURCE_PASSWORD", databaseType.getPassword()) + .withEnv("SPRING_ELASTICSEARCH_REST_URIS", "http://elasticsearch:9200") + .withEnv("ELASTICSEARCH_INDEXNAME", CUSTOM_ALFRESCO_INDEX) + .withEnv("ALFRESCO_ACCEPTEDCONTENTMEDIATYPESCACHE_BASEURL", "http://transform-core-aio:8090/transform/config") + .withEnv("ALFRESCO_ACS_URL", "http://alfresco:8080") + .withEnv("ALFRESCO_CONTENT_TRANSFORM_SHAREDSECRET", "secret") + .withEnv("ALFRESCO_REINDEX_BATCHSIZE", "1000") + .withEnv("ALFRESCO_REINDEX_PAGESIZE", "1000") + .withEnv("ALFRESCO_REINDEX_CONTINUOUS_POLLINGINTERVAL", "1s") + .withEnv("ALFRESCO_REINDEX_CONTINUOUS_CATCHUPPOLLINGINTERVAL", "100ms") + .withFileSystemBind(driverJarPath, "/opt/db-drivers/" + driverJarName, BindMode.READ_ONLY) + .withExposedPorts(5005); + } + + private static String resolveDriverJarPath(String driverClassName) + { + try + { + Class driverClass = Class.forName(driverClassName); + URI jarUri = driverClass.getProtectionDomain().getCodeSource().getLocation().toURI(); + return Paths.get(jarUri).toString(); + } + catch (Exception e) + { + throw new IllegalStateException("Could not resolve JAR for JDBC driver class: " + driverClassName, e); + } + } + + protected GenericContainer createSearchEngineContainer() + { + return getImagesConfig().getSearchEngineType() == SearchEngineType.OPENSEARCH_ENGINE ? createOpensearchContainer() : createElasticContainer(); + } + + protected GenericContainer createDashboardsContainer() + { + return getImagesConfig().getSearchEngineType() == SearchEngineType.OPENSEARCH_ENGINE ? createOpensearchDashboardsContainer() : createKibanaContainer(); + } + + protected GenericContainer createElasticContainer() + { + return new GenericContainer<>(getImagesConfig().getElasticsearchImage()) + .withNetwork(network) + .withNetworkAliases("elasticsearch") + .withExposedPorts(9200) + .withEnv("xpack.security.enabled", "false") + .withEnv("discovery.type", "single-node") + .withEnv("ES_JAVA_OPTS", "-Xms2g -Xmx2g"); + } + + protected GenericContainer createOpensearchContainer() + { + return new GenericContainer<>(getImagesConfig().getOpensearchImage()) + .withNetwork(network) + .withNetworkAliases("elasticsearch") + .withExposedPorts(9200) + .withEnv("plugins.security.disabled", "true") + .withEnv("discovery.type", "single-node") + .withEnv("OPENSEARCH_JAVA_OPTS", "-Xms2g -Xmx2g"); + } + + protected GenericContainer createOpensearchDashboardsContainer() + { + return new GenericContainer(getImagesConfig().getOpensearchDashboardsImage()) + .withNetwork(network) + .withNetworkAliases("kibana") + .withExposedPorts(5601) + .withEnv("ELASTICSEARCH_HOSTS", "http://elasticsearch:9200"); + } + + protected GenericContainer createKibanaContainer() + { + return new GenericContainer(getImagesConfig().getKibanaImage()) + .withNetwork(network) + .withNetworkAliases("kibana") + .withExposedPorts(5601) + .withEnv("ELASTICSEARCH_HOSTS", "http://elasticsearch:9200"); + } + + private GenericContainer createAMQContainer() + { + return new GenericContainer(getImagesConfig().getActiveMqImage()) + .withNetwork(network) + .withNetworkAliases("activemq") + .withEnv("JAVA_OPTS", "-Xms512m -Xmx1g") + .waitingFor(Wait.forListeningPort()) + .withStartupTimeout(Duration.ofMinutes(2)) + .withExposedPorts(61616, 8161, 5672, 61613); + } + + private PostgreSQLContainer createPosgresContainer() + { + return (PostgreSQLContainer) new PostgreSQLContainer(getImagesConfig().getPostgreSQLImage()) + .withPassword(DatabaseType.POSTGRESQL_DB.getPassword()) + .withUsername(DatabaseType.POSTGRESQL_DB.getUsername()) + .withDatabaseName("alfresco") + .withNetwork(network) + .withNetworkAliases("postgres") + .withStartupTimeout(Duration.ofMinutes(2)); + } + + private MySQLContainer createMySqlContainer() + { + return (MySQLContainer) new MySQLContainer(getImagesConfig().getMySQLImage()) + .withPassword(DatabaseType.MYSQL_DB.getPassword()) + .withUsername(DatabaseType.MYSQL_DB.getUsername()) + .withDatabaseName("alfresco") + .withNetwork(network) + .withNetworkAliases("mysql") + .withStartupTimeout(Duration.ofMinutes(2)); + + } + + private MariaDBContainer createMariaDBContainer() + { + return new MariaDBContainer<>(getImagesConfig().getMariaDBImage()) + .withPassword(DatabaseType.MARIA_DB.getPassword()) + .withUsername(DatabaseType.MARIA_DB.getUsername()) + .withDatabaseName("alfresco") + .withNetwork(network) + .withNetworkAliases("mariadb") + .withStartupTimeout(Duration.ofMinutes(2)); + + } + + private GenericContainer createTransformCoreContainer() + { + return new GenericContainer(getImagesConfig().getTransformCoreAIOImage()) + .withNetwork(network) + .withNetworkAliases("transform-core-aio") + .withEnv("JAVA_OPTS", "-Xms512m -Xmx1024m") + .withEnv("ACTIVEMQ_URL", "nio://activemq:61616") + .withEnv("ACTIVEMQ_USER", "admin") + .withEnv("ACTIVEMQ_PASSWORD", "admin") + .withEnv("FILE_STORE_URL", "http://shared-file-store:8099/alfresco/api/-default-/private/sfs/versions/1/file") + .withExposedPorts(8090) + .waitingFor(Wait.forListeningPort()) + .withStartupTimeout(Duration.ofMinutes(2)); + } + + protected GenericContainer createAlfrescoContainer() + { + DatabaseType databaseType = getImagesConfig().getDatabaseType(); + return new GenericContainer(getImagesConfig().getRepositoryImage()) + .withEnv("CATALINA_OPTS", "\"-agentlib:jdwp=transport=dt_socket,address=*:8000,server=y,suspend=n\"") + .withEnv("JAVA_TOOL_OPTIONS", + "-Dencryption.keystore.type=JCEKS " + + "-Dencryption.cipherAlgorithm=DESede/CBC/PKCS5Padding " + + "-Dencryption.keyAlgorithm=DESede " + + "-Dencryption.keystore.location=/usr/local/tomcat/shared/classes/alfresco/extension/keystore/keystore " + + "-Dmetadata-keystore.password=mp6yc0UD9e -Dmetadata-keystore.aliases=metadata " + + "-Dmetadata-keystore.metadata.password=oKIWzVdEdA -Dmetadata-keystore.metadata.algorithm=DESede") + .withEnv("JAVA_OPTS", + "-Delasticsearch.createIndexIfNotExists=true " + + "-Dindex.subsystem.name=elasticsearch " + + "-Delasticsearch.host=elasticsearch " + + "-Dsolr.secureComms=secret " + + "-Dsolr.sharedSecret=secret " + + "-Delasticsearch.indexName=" + CUSTOM_ALFRESCO_INDEX + " " + + "-Ddb.driver=" + databaseType.getDriver() + " " + + "-Ddb.url=" + escapeSemicolonInUrlForJavaOptsUsage(databaseType.getUrl()) + " " + + "-Ddb.username=" + databaseType.getUsername() + " " + + "-Ddb.password=" + databaseType.getPassword() + " " + + indentDbSettings(databaseType.getAdditionalDbSettings()) + + "-Dshare.host=127.0.0.1 " + + "-Dshare.port=8080 " + + "-Dalfresco.host=localhost " + + "-Dalfresco.port=8080 " + + "-Daos.baseUrlOverwrite=http://localhost:8080/alfresco/aos " + + "-Dmessaging.broker.url=\"failover:(nio://activemq:61616)?timeout=3000&jms.useCompression=true\" " + + "-Dmessaging.broker.username=admin " + + "-Dmessaging.broker.password=admin " + + "-Ddeployment.method=DOCKER_COMPOSE " + + "-Dtransform.service.enabled=true " + + "-Dtransform.service.url=http://transform-router:8095 " + + "-Dsfs.url=http://shared-file-store:8099 " + + "-DlocalTransform.core-aio.url=http://transform-core-aio:8090/ " + + "-Dcsrf.filter.enabled=false " + + "-Dalfresco.restApi.basicAuthScheme=true " + + "-Dquery.cmis.queryConsistency=EVENTUAL " + + "-Dquery.fts.queryConsistency=EVENTUAL " + + "-Xms1g -Xmx2g ") + .withNetwork(network) + .withNetworkAliases("alfresco") + .waitingFor(new LogMessageWaitStrategy().withRegEx(".*Server startup in.*\\n")) + .withStartupTimeout(Duration.ofMinutes(7)) + .withExposedPorts(8080, 8000) + .withClasspathResourceMapping("exactTermSearch.properties", + "/usr/local/tomcat/webapps/alfresco/WEB-INF/classes/alfresco/search/elasticsearch/config/exactTermSearch.properties", + BindMode.READ_ONLY); + } + + private String escapeSemicolonInUrlForJavaOptsUsage(String url) + { + return url.replace(";", "\\;"); + } + + private String indentDbSettings(Map additionalDbSettings) + { + StringBuilder sb = new StringBuilder(); + for (Map.Entry setting : additionalDbSettings.entrySet()) + { + sb.append("-Ddb.").append(setting.getKey()).append("=").append(setting.getValue()).append(" "); + } + return sb.toString(); + } + + public static ImagesConfig getImagesConfig() + { + return DefaultImagesConfig.INSTANCE; + } + + public interface ImagesConfig + { + String getBatchIndexingImage(); + + String getElasticsearchImage(); + + String getOpensearchImage(); + + String getOpensearchDashboardsImage(); + + String getActiveMqImage(); + + String getTransformCoreAIOImage(); + + String getPostgreSQLImage(); + + String getMySQLImage(); + + String getMariaDBImage(); + + DatabaseType getDatabaseType(); + + String getRepositoryImage(); + + String getKibanaImage(); + + SearchEngineType getSearchEngineType(); + } + + private record DefaultImagesConfig(Function envProperties, Function mavenProperties) implements ImagesConfig + { + private static final DefaultImagesConfig INSTANCE = new DefaultImagesConfig(EnvHelper::getEnvProperty, MavenPropertyHelper::getMavenProperty); + + @Override + public String getBatchIndexingImage() + { + return getSystemProperty("indeximage", "alfresco/alfresco-elasticsearch-batch-indexing:local"); + } + + @Override + public String getElasticsearchImage() + { + return "docker.elastic.co/elasticsearch/elasticsearch:" + envProperties.apply("ELASTICSEARCH_TAG"); + } + + @Override + public String getOpensearchImage() + { + return "opensearchproject/opensearch:" + envProperties.apply("OPENSEARCH_TAG"); + } + + @Override + public String getOpensearchDashboardsImage() + { + return "opensearchproject/opensearch-dashboards:" + envProperties.apply("OPENSEARCH_DASHBOARDS_TAG"); + } + + @Override + public String getActiveMqImage() + { + return "alfresco/alfresco-activemq:" + envProperties.apply("ACTIVEMQ_TAG"); + } + + @Override + public String getTransformCoreAIOImage() + { + return "alfresco/alfresco-transform-core-aio:" + mavenProperties.apply("dependency.alfresco-transform-core.version"); + } + + @Override + public String getPostgreSQLImage() + { + return "postgres:" + envProperties.apply("POSTGRES_TAG"); + } + + @Override + public String getMySQLImage() + { + return "mysql:" + envProperties.apply("MYSQL_TAG"); + } + + @Override + public String getMariaDBImage() + { + return "mariadb:" + envProperties.apply("MARIADB_TAG"); + } + + @Override + public String getRepositoryImage() + { + return getSystemProperty("repoimage", "alfresco/alfresco-content-repository-community:latest"); + } + + @Override + public String getKibanaImage() + { + return "kibana:" + envProperties.apply("KIBANA_TAG"); + } + + @Override + public SearchEngineType getSearchEngineType() + { + String searchEngineTypeProperty = mavenProperties.apply("search.engine.type"); + if (Strings.isNullOrEmpty(searchEngineTypeProperty)) + { + Step.STEP("Defaulting search engine to Elasticsearch."); + return SearchEngineType.ELASTICSEARCH_ENGINE; + } + return SearchEngineType.from(searchEngineTypeProperty); + } + + @Override + public DatabaseType getDatabaseType() + { + String databaseTypeProperty = mavenProperties.apply("database.type"); + if (Strings.isNullOrEmpty(databaseTypeProperty)) + { + Step.STEP("Defaulting database to postgresql."); + return DatabaseType.POSTGRESQL_DB; + } + return DatabaseType.from(databaseTypeProperty); + } + } + +} diff --git a/tests/testcontainers-env/src/main/java/org/alfresco/tas/DatabaseType.java b/tests/testcontainers-env/src/main/java/org/alfresco/tas/DatabaseType.java new file mode 100644 index 000000000..b77a3cc81 --- /dev/null +++ b/tests/testcontainers-env/src/main/java/org/alfresco/tas/DatabaseType.java @@ -0,0 +1,100 @@ +/* + * #%L + * Alfresco Testcontainers Environment + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.tas; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public enum DatabaseType +{ + POSTGRESQL_DB("postgresql", "org.postgresql.Driver", "jdbc:postgresql://postgres:5432/alfresco", "alfresco", "alfresco"), MYSQL_DB("mysql", "com.mysql.cj.jdbc.Driver", "jdbc:mysql://mysql:3306/alfresco", "alfresco", "alfresco"), MARIA_DB("mariadb", "org.mariadb.jdbc.Driver", "jdbc:mariadb://mariadb:3306/alfresco", "alfresco", "alfresco"); + + private final String type; + private final String driver; + private final String url; + private final String username; + private final String password; + private final Map additionalDbSettings; + + DatabaseType(String type, String driver, String url, String username, String password, Map additionalDbSettings) + { + this.type = type; + this.driver = driver; + this.url = url; + this.username = username; + this.password = password; + this.additionalDbSettings = additionalDbSettings; + } + + DatabaseType(String type, String driver, String url, String username, String password) + { + this.type = type; + this.driver = driver; + this.url = url; + this.username = username; + this.password = password; + this.additionalDbSettings = new HashMap<>(0); + } + + public String getType() + { + return this.type; + } + + public String getDriver() + { + return driver; + } + + public String getUrl() + { + return url; + } + + public String getUsername() + { + return username; + } + + public String getPassword() + { + return password; + } + + public Map getAdditionalDbSettings() + { + return additionalDbSettings; + } + + public static DatabaseType from(String type) + { + return Arrays.stream(DatabaseType.values()) + .filter(database -> database.getType().equals(type.toLowerCase())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Database of type + '" + type + "' not defined.")); + } +} diff --git a/tests/testcontainers-env/src/main/java/org/alfresco/tas/EnvHelper.java b/tests/testcontainers-env/src/main/java/org/alfresco/tas/EnvHelper.java new file mode 100644 index 000000000..d97990d45 --- /dev/null +++ b/tests/testcontainers-env/src/main/java/org/alfresco/tas/EnvHelper.java @@ -0,0 +1,73 @@ +/* + * #%L + * Alfresco Testcontainers Environment + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.tas; + +import java.io.FileReader; +import java.util.Properties; + +import org.testng.Assert; + +/** A helper class with methods to access properties from the .env file. */ +public class EnvHelper +{ + /** The location of the .env file (relative to the root of this maven submodule). */ + private static final String ENV_FILE_NAME = "../environment/.env"; + + /** Private constructor for helper class. */ + private EnvHelper() + {} + + /** + * Load the value of a property from the .env file. + * + * @param key + * The key to look up. + * @return The value as a string. + */ + public static String getEnvProperty(String key) + { + return loadEnvProperties().getProperty(key); + } + + /** + * Load all the properties from the .env file. + * + * @return The properties. + */ + public static Properties loadEnvProperties() + { + Properties env = new Properties(); + try (FileReader reader = new FileReader(ENV_FILE_NAME)) + { + env.load(reader); + } + catch (Exception e) + { + Assert.fail("unable to load .env property file "); + } + return env; + } +} diff --git a/tests/testcontainers-env/src/main/java/org/alfresco/tas/MavenPropertyHelper.java b/tests/testcontainers-env/src/main/java/org/alfresco/tas/MavenPropertyHelper.java new file mode 100644 index 000000000..3b991702b --- /dev/null +++ b/tests/testcontainers-env/src/main/java/org/alfresco/tas/MavenPropertyHelper.java @@ -0,0 +1,83 @@ +/* + * #%L + * Alfresco Testcontainers Environment + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.tas; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.Properties; + +import org.testng.Assert; + +/** A helper class with methods to access properties from Maven pom files. */ +public class MavenPropertyHelper +{ + /** The relative path to the maven property file resource. */ + private static final String MAVEN_PROPERTIES_FILE = "maven.properties"; + + /** Private constructor for helper class. */ + private MavenPropertyHelper() + {} + + /** + * Load the value of a property from Maven. + * + * @param key + * The key to look up. + * @return The value as a string. + */ + public static String getMavenProperty(String key) + { + String value = loadMavenProperties().getProperty(key); + // If Maven filtering hasn't run, the value will be an + // unresolved placeholder like "${database.type}". Fall back to the JVM system property instead. + if (value != null && value.startsWith("${") && value.endsWith("}")) + { + value = System.getProperty(key); + } + return value; + } + + /** + * Load all the properties from Maven. + * + * @return The properties. + */ + public static Properties loadMavenProperties() + { + Properties properties = new Properties(); + try (InputStream inputStream = MavenPropertyHelper.class.getClassLoader().getResourceAsStream(MAVEN_PROPERTIES_FILE)) + { + Reader reader = new InputStreamReader(inputStream); + properties.load(reader); + } + catch (Exception e) + { + Assert.fail("Unable to load maven.properties file "); + } + return properties; + } +} diff --git a/tests/testcontainers-env/src/main/java/org/alfresco/tas/SearchEngineType.java b/tests/testcontainers-env/src/main/java/org/alfresco/tas/SearchEngineType.java new file mode 100644 index 000000000..4e5065501 --- /dev/null +++ b/tests/testcontainers-env/src/main/java/org/alfresco/tas/SearchEngineType.java @@ -0,0 +1,53 @@ +/* + * #%L + * Alfresco Testcontainers Environment + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.tas; + +import java.util.Arrays; + +public enum SearchEngineType +{ + OPENSEARCH_ENGINE("opensearch"), ELASTICSEARCH_ENGINE("elasticsearch"); + + private final String type; + + SearchEngineType(String type) + { + this.type = type; + } + + public String getType() + { + return this.type; + } + + public static SearchEngineType from(String type) + { + return Arrays.stream(SearchEngineType.values()) + .filter(engine -> engine.getType().equals(type.toLowerCase())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Search engine of type '" + type + "' not defined.")); + } +} diff --git a/tests/testcontainers-env/src/main/java/org/alfresco/tas/SystemPropertyHelper.java b/tests/testcontainers-env/src/main/java/org/alfresco/tas/SystemPropertyHelper.java new file mode 100644 index 000000000..651f991bd --- /dev/null +++ b/tests/testcontainers-env/src/main/java/org/alfresco/tas/SystemPropertyHelper.java @@ -0,0 +1,53 @@ +/* + * #%L + * Alfresco Testcontainers Environment + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.tas; + +/** A helper class with methods to access system properties, e.g. those passed via -D maven command line arguments. */ +public class SystemPropertyHelper +{ + /** Private constructor for helper class. */ + private SystemPropertyHelper() + {} + + /** + * Load the value of a property. + * + * @param key + * The key to look up. + * @param defaultValue + * The default value to use if the property is not set. + * @return The value as a string. + */ + public static String getSystemProperty(String key, String defaultValue) + { + String value = System.getProperty(key); + if (value == null) + { + return defaultValue; + } + return value; + } +} diff --git a/tests/testcontainers-env/src/main/java/org/alfresco/tas/TestDataUtility.java b/tests/testcontainers-env/src/main/java/org/alfresco/tas/TestDataUtility.java new file mode 100644 index 000000000..3628652b8 --- /dev/null +++ b/tests/testcontainers-env/src/main/java/org/alfresco/tas/TestDataUtility.java @@ -0,0 +1,46 @@ +/* + * #%L + * Alfresco Testcontainers Environment + * %% + * Copyright (C) 2026 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.tas; + +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** A class providing utility methods for e2e testing. */ +public class TestDataUtility +{ + private static final Random RANDOM = new Random(); + private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + /** Generate a random 'word' containing 16 alphabetic characters. */ + public static String getAlphabeticUUID() + { + return IntStream.range(0, 16) + .map(i -> ALPHABET.charAt(RANDOM.nextInt(ALPHABET.length()))) + .mapToObj(Character::toString) + .collect(Collectors.joining()); + } +} diff --git a/tests/testcontainers-env/src/main/resources/maven.properties b/tests/testcontainers-env/src/main/resources/maven.properties new file mode 100644 index 000000000..e42237dca --- /dev/null +++ b/tests/testcontainers-env/src/main/resources/maven.properties @@ -0,0 +1,5 @@ +database.type=${database.type} +search.engine.type=${search.engine.type} +dependency.alfresco-transform-service.version=${dependency.alfresco-transform-service.version} +dependency.alfresco-transform-core.version=${dependency.alfresco-transform-core.version} +