diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b036797ed..2402ec76c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -626,6 +626,10 @@ jobs: pom-dir: tas-webdav - test-name: "Integration TAS tests" pom-dir: tas-integration + env: + REQUIRES_LOCAL_IMAGES: true + REQUIRES_TAS_TESTS: true + REQUIRES_INSTALLED_ARTIFACTS: true steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: Alfresco/alfresco-build-tools/.github/actions/get-build-info@v18.20.0 @@ -648,9 +652,6 @@ jobs: run: | ${{ env.TAS_SCRIPTS }}/start-compose.sh ${{ env.TAS_ENVIRONMENT }}/docker-compose-minimal+transforms.yml ${{ env.TAS_SCRIPTS }}/wait-for-alfresco-start.sh "http://localhost:8082/alfresco" - - name: "Build TAS integration tests" - if: ${{ matrix.test-name }} == 'Integration TAS tests' - run: mvn install -pl :alfresco-community-repo-integration-test -am -DskipTests -Pall-tas-tests - name: "Run tests" id: tests timeout-minutes: ${{ fromJSON(env.GITHUB_ACTIONS_DEPLOY_TIMEOUT) }} diff --git a/.secrets.baseline b/.secrets.baseline index 209fcecf5b..2c4a9c5e49 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -153,7 +153,7 @@ "filename": "amps/ags/rm-automation/rm-automation-community-rest-api/src/test/resources/default.properties", "hashed_secret": "d033e22ae348aeb5660fc2140aec35850c4da997", "is_verified": false, - "line_number": 21, + "line_number": 25, "is_secret": false }, { @@ -161,7 +161,7 @@ "filename": "amps/ags/rm-automation/rm-automation-community-rest-api/src/test/resources/default.properties", "hashed_secret": "a4a747bd4ba5e3a5049cad116881867c71fb625b", "is_verified": false, - "line_number": 87, + "line_number": 91, "is_secret": false } ], @@ -516,7 +516,7 @@ "filename": "packaging/tests/tas-restapi/src/main/resources/default.properties", "hashed_secret": "d033e22ae348aeb5660fc2140aec35850c4da997", "is_verified": false, - "line_number": 21, + "line_number": 25, "is_secret": false }, { @@ -524,17 +524,25 @@ "filename": "packaging/tests/tas-restapi/src/main/resources/default.properties", "hashed_secret": "a4a747bd4ba5e3a5049cad116881867c71fb625b", "is_verified": false, - "line_number": 87, + "line_number": 91, "is_secret": false } ], "packaging/tests/tas-restapi/src/test/resources/default.properties": [ + { + "type": "Secret Keyword", + "filename": "packaging/tests/tas-restapi/src/test/resources/default.properties", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 15, + "is_secret": false + }, { "type": "Secret Keyword", "filename": "packaging/tests/tas-restapi/src/test/resources/default.properties", "hashed_secret": "d033e22ae348aeb5660fc2140aec35850c4da997", "is_verified": false, - "line_number": 21, + "line_number": 23, "is_secret": false }, { @@ -542,7 +550,7 @@ "filename": "packaging/tests/tas-restapi/src/test/resources/default.properties", "hashed_secret": "a4a747bd4ba5e3a5049cad116881867c71fb625b", "is_verified": false, - "line_number": 87, + "line_number": 99, "is_secret": false } ], diff --git a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/resources/default.properties b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/resources/default.properties index 0586dd982d..802c983d17 100644 --- a/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/resources/default.properties +++ b/amps/ags/rm-automation/rm-automation-community-rest-api/src/test/resources/default.properties @@ -16,6 +16,10 @@ solr.port=8083 #Solr Indexing Time solrWaitTimeInSeconds=20 +# CMIS settings - required by cmis dependency +cmis.binding=browser +cmis.basePath=/alfresco/api/-default-/public/cmis/versions/1.1/${cmis.binding} + # credentials admin.user=admin admin.password=admin diff --git a/packaging/tests/pom.xml b/packaging/tests/pom.xml index fc60423bf5..522b8c603e 100644 --- a/packaging/tests/pom.xml +++ b/packaging/tests/pom.xml @@ -10,10 +10,10 @@ - tas-restapi tas-cmis tas-email tas-webdav + tas-restapi tas-integration diff --git a/packaging/tests/tas-restapi/pom.xml b/packaging/tests/tas-restapi/pom.xml index 0fca618f37..84c9fb9e6d 100644 --- a/packaging/tests/tas-restapi/pom.xml +++ b/packaging/tests/tas-restapi/pom.xml @@ -41,6 +41,12 @@ ${project.basedir}/src/test/resources/test-suites/part3-suite.xml + + elasticsearch-e2e-tests + + ${project.basedir}/src/test/resources/elasticsearch-e2e-suite.xml + + @@ -179,6 +185,13 @@ org.eclipse.parsson parsson + + + org.alfresco.tas + cmis + ${project.version} + test + diff --git a/packaging/tests/tas-restapi/src/main/resources/default.properties b/packaging/tests/tas-restapi/src/main/resources/default.properties index 268003078d..4b5cc1e420 100644 --- a/packaging/tests/tas-restapi/src/main/resources/default.properties +++ b/packaging/tests/tas-restapi/src/main/resources/default.properties @@ -16,6 +16,10 @@ solr.port=8083 #Solr Indexing Time solrWaitTimeInSeconds=20 +## CMIS settings - required by search E2E tests +cmis.binding=browser +cmis.basePath=/alfresco/api/-default-/public/cmis/versions/1.1/${cmis.binding} + # credentials admin.user=admin admin.password=admin diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/AbstractE2EFunctionalTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/AbstractE2EFunctionalTest.java new file mode 100644 index 0000000000..8737eaa765 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/AbstractE2EFunctionalTest.java @@ -0,0 +1,560 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import static java.util.Optional.ofNullable; + +import static org.testng.Assert.assertEquals; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.hamcrest.Matchers; +import org.slf4j.Logger; +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.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; + +import org.alfresco.cmis.CmisWrapper; +import org.alfresco.dataprep.SiteService.Visibility; +import org.alfresco.rest.core.RestWrapper; +import org.alfresco.rest.exception.EmptyJsonResponseException; +import org.alfresco.rest.model.RestRequestSpellcheckModel; +import org.alfresco.utility.LogFactory; +import org.alfresco.utility.TasProperties; +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.CustomContentModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.UserModel; +import org.alfresco.utility.network.ServerHealth; + +/** + * Base Spring-bootstrapped functional test class for Search API E2E tests. Owns: Spring context, autowired services, common user/site setup, generic search/query helpers, indexing-wait helpers, spellcheck helpers and custom-model lifecycle helpers. + *

+ * Mirrors the structure of {@code AbstractE2EFunctionalTest} from the InsightEngine repo, which is the source of truth for these tests. + */ +@ContextConfiguration("classpath:alfresco-restapi-context.xml") +@SuppressWarnings({"PMD.CouplingBetweenObjects", "PMD.GodClass", "PMD.TooManyMethods"}) +public abstract class AbstractE2EFunctionalTest extends AbstractTestNGSpringContextTests +{ + private static final Logger LOGGER = LogFactory.getLogger(); + + /** Maximum number of times a wait-for-indexing query will be retried before giving up. */ + protected static final int SEARCH_MAX_ATTEMPTS = 120; + + @Autowired + protected TasProperties properties; + + @Autowired + protected ServerHealth serverHealth; + + @Autowired + protected DataSite dataSite; + + @Autowired + protected DataContent dataContent; + + @Autowired + protected RestWrapper restClient; + + @Autowired + protected CmisWrapper cmisApi; + + @Autowired + protected DataUser dataUser; + + protected UserModel testUser; + protected UserModel adminUserModel; + protected UserModel testUser2; + + protected SiteModel testSite; + protected SiteModel testSite2; + + /** Random unique string derived from the test site name; used to build unique file titles. */ + @SuppressWarnings({"PMD.MutableStaticState", "PMD.FieldNamingConventions"}) + protected static String unique_searchString; + + /** Search query language. {@link #toString()} returns the lower-case identifier expected by Search API. */ + protected enum SearchLanguage + { + CMIS, AFTS; + + @Override + @SuppressWarnings("PMD.UseLocaleWithCaseConversions") + public String toString() + { + return name().toLowerCase(); + } + } + + @BeforeClass(alwaysRun = true) + public void setup() + { + serverHealth.assertServerIsOnline(); + + adminUserModel = dataUser.getAdminUser(); + testUser = dataUser.createRandomTestUser("UserSearch"); + + testSite = new SiteModel(RandomData.getRandomName("SiteSearch")); + testSite.setVisibility(Visibility.PRIVATE); + testSite = dataSite.usingUser(testUser).createSite(testSite); + + unique_searchString = testSite.getTitle().replace("SiteSearch", "Unique"); + } + + @BeforeMethod(alwaysRun = true) + public void showStartTestInfo(Method method) + { + LOGGER.info(String.format("*** STARTING Test: [%s] ***", method.getName())); + } + + @AfterMethod(alwaysRun = true) + public void showEndTestInfo(Method method) + { + LOGGER.info(String.format("*** ENDING Test: [%s] ***", method.getName())); + } + + // ------------------------------------------------------------------------- + // Custom content model lifecycle + // ------------------------------------------------------------------------- + + /** Deploys a custom content model from the given classpath path. Path must end with {@code -model.xml}. */ + public boolean deployCustomModel(String path) + { + if (path == null || !path.endsWith("-model.xml")) + { + return false; + } + try + { + dataContent.usingAdmin().deployContentModel(path); + return true; + } + catch (Exception e) + { + LOGGER.warn("Error loading custom model: " + path, e); + return false; + } + } + + /** Deactivates a deployed custom content model via the private CMM REST API. */ + public boolean deactivateCustomModel(String modelFileName) + { + try + { + CustomContentModel model = new CustomContentModel(); + model.setName(fileNameToModelLocalName(modelFileName)); + restClient.authenticateUser(adminUserModel).withPrivateAPI().usingCustomModel(model).deactivateModel(); + return true; + } + catch (Exception e) + { + LOGGER.warn("Error deactivating custom model: " + modelFileName, e); + return false; + } + } + + /** Deletes a deactivated custom content model via the private CMM REST API. */ + public boolean deleteCustomModel(String modelFileName) + { + try + { + CustomContentModel model = new CustomContentModel(); + model.setName(fileNameToModelLocalName(modelFileName)); + restClient.authenticateUser(adminUserModel).withPrivateAPI().usingCustomModel(model).deleteModel(); + return true; + } + catch (Exception e) + { + LOGGER.warn("Error deleting custom model: " + modelFileName, e); + return false; + } + } + + /** + * Converts a model file name like {@code "sharding-content-model.xml"} (or {@code "model/sharding-content-model.xml"}) into its CamelCase local name {@code "ShardingContentModel"} as expected by the private CMM REST API. + */ + @SuppressWarnings({"PMD.UseIndexOfChar", "PMD.UseLocaleWithCaseConversions"}) + private String fileNameToModelLocalName(String modelFileName) + { + String base = modelFileName.contains("/") + ? modelFileName.substring(modelFileName.lastIndexOf("/") + 1) + : modelFileName; + if (base.endsWith(".xml")) + { + base = base.substring(0, base.length() - 4); + } + StringBuilder sb = new StringBuilder(); + for (String part : base.split("-")) + { + if (!part.isEmpty()) + { + sb.append(Character.toUpperCase(part.charAt(0))).append(part.substring(1).toLowerCase()); + } + } + return sb.toString(); + } + + protected DataUser getDataUser() + { + return dataUser; + } + + // ------------------------------------------------------------------------- + // Query helpers + // ------------------------------------------------------------------------- + + /** Executes a {@link SearchRequest} as {@link #testUser}. */ + protected SearchResponse query(SearchRequest query) + { + return restClient.authenticateUser(testUser).withSearchAPI().search(query); + } + + /** Runs a simple AFTS query as admin and returns the response. */ + public SearchResponse query(String queryString) + { + return queryAsUser(dataUser.getAdminUser(), queryString); + } + + /** Runs a search with the given query and highlight model as {@link #testUser}. */ + protected SearchResponse query(RestRequestQueryModel queryReq, RestRequestHighlightModel highlight) + { + SearchRequest searchReq = new SearchRequest(queryReq); + searchReq.setHighlight(highlight); + return restClient.authenticateUser(testUser).withSearchAPI().search(searchReq); + } + + /** Runs an AFTS query as the given user. */ + protected SearchResponse queryAsUser(UserModel user, String queryString) + { + SearchRequest searchRequest = new SearchRequest(); + RestRequestQueryModel queryModel = new RestRequestQueryModel(); + queryModel.setQuery(queryString); + searchRequest.setQuery(queryModel); + return restClient.authenticateUser(user).withSearchAPI().search(searchRequest); + } + + /** Runs the given query and spellcheck model as the given user. */ + protected SearchResponse queryAsUser(UserModel user, RestRequestQueryModel queryModel, RestRequestSpellcheckModel spellcheckQuery) + { + SearchRequest searchRequest = new SearchRequest(); + searchRequest.setQuery(queryModel); + searchRequest.setSpellcheck(spellcheckQuery); + return restClient.authenticateUser(user).withSearchAPI().search(searchRequest); + } + + /** Runs the given query as the given user, optionally with pagination. */ + protected SearchResponse queryAsUser(UserModel user, RestRequestQueryModel queryModel, Pagination paging) + { + SearchRequest searchRequest = new SearchRequest(); + searchRequest.setQuery(queryModel); + if (paging != null) + { + searchRequest.setPaging(paging); + } + return restClient.authenticateUser(user).withSearchAPI().search(searchRequest); + } + + /** Builds a {@link SearchRequest} from a single AFTS term. */ + protected SearchRequest createQuery(String term) + { + SearchRequest query = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery(term); + query.setQuery(queryReq); + return query; + } + + // ------------------------------------------------------------------------- + // Wait-for-indexing helpers + // ------------------------------------------------------------------------- + + /** Waits using a free-form query (no field prefix injected). */ + public boolean waitForIndexing(String userQuery, boolean expectedInResults) + { + return waitForIndexing(null, userQuery, expectedInResults); + } + + /** Waits for {@code name:'userQuery'} (metadata is indexed before content). */ + public boolean waitForMetadataIndexing(String userQuery, boolean expectedInResults) + { + return waitForIndexing("name", userQuery, expectedInResults); + } + + /** Waits for {@code cm:content:'userQuery'} (content indexing can be slower than metadata). */ + public boolean waitForContentIndexing(String userQuery, boolean expectedInResults) + { + return waitForIndexing("cm:content", userQuery, expectedInResults); + } + + private boolean waitForIndexing(String fieldName, String userQuery, boolean expectedInResults) + { + String query = (fieldName == null) ? userQuery : String.format("%s:'%s'", fieldName, userQuery); + return isContentInSearchResults(query, null, expectedInResults); + } + + /** + * Polls the search API until the expected content appears or is absent from the results, or until {@link #SEARCH_MAX_ATTEMPTS} attempts have elapsed. Uses the configured Solr wait time per retry attempt. + */ + public boolean isContentInSearchResults(String userQuery, String contentToFind, boolean expectedInResults) + { + String contentName = (contentToFind == null) ? "" : contentToFind; + SearchRequest searchRequest = createQuery(userQuery); + final int ignoreRuntimeExceptionThreshold = SEARCH_MAX_ATTEMPTS / 10 + 1; + + for (int searchCount = 0; searchCount < SEARCH_MAX_ATTEMPTS; searchCount++) + { + try + { + if (expectedInResults == isContentFoundWithRequest(searchRequest, contentName)) + { + return true; + } + } + catch (EmptyJsonResponseException ignore) + { + // Empty response — try again + } + catch (RuntimeException runtimeException) + { + if (searchCount > ignoreRuntimeExceptionThreshold) + { + throw runtimeException; + } + LOGGER.warn("Ignoring initial Search API failure.", runtimeException); + } + finally + { + Utility.waitToLoopTime(properties.getSolrWaitTimeInSeconds(), + "Wait For Indexing. Retry Attempt: " + (searchCount + 1)); + } + } + return false; + } + + @SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes") + private boolean isContentFoundWithRequest(SearchRequest searchRequest, String contentName) + { + SearchResponse response = query(searchRequest); + if (restClient.getStatusCode().matches(String.valueOf(HttpStatus.OK.value()))) + { + return isContentInSearchResponse(response, contentName); + } + String responseBody = restClient.onResponse().getResponse().body().prettyPrint(); + throw new RuntimeException("API returned status code: " + restClient.getStatusCode() + + " Expected: " + HttpStatus.OK.value() + "; Response body: " + responseBody); + } + + /** True if a result entry's name matches {@code contentName} (case-insensitive), or if {@code contentName} is blank. */ + public boolean isContentInSearchResponse(SearchResponse response, String contentName) + { + return response.getEntries().stream() + .map(entry -> entry.getModel().getName()) + .anyMatch(name -> name.equalsIgnoreCase(contentName) || contentName.isBlank()); + } + + // ------------------------------------------------------------------------- + // testSearchQuery* helpers + // ------------------------------------------------------------------------- + + /** Runs a search and (optionally) asserts the {@code count} in the pagination response. Defaults to AFTS. */ + protected SearchResponse testSearchQuery(String query, Integer expectedCount) + { + return testSearchQuery(query, expectedCount, SearchLanguage.AFTS); + } + + /** Runs a search using the given language and (optionally) asserts the {@code count} in the pagination response. */ + protected SearchResponse testSearchQuery(String query, Integer expectedCount, SearchLanguage queryLanguage) + { + SearchResponse response = performSearch(testUser, query, queryLanguage, getDefaultPagingOptions()); + if (expectedCount != null) + { + restClient.onResponse().assertThat().body("list.pagination.count", Matchers.equalTo(expectedCount)); + } + return response; + } + + /** Runs a search and asserts that the returned entry names exactly match {@code expectedNames} in the given order. */ + protected SearchResponse testSearchQueryOrdered(String query, List expectedNames, SearchLanguage queryLanguage) + { + SearchResponse response = performSearch(testUser, query, queryLanguage, getDefaultPagingOptions()); + List names = response.getEntries().stream() + .map(s -> s.getModel().getName()) + .collect(Collectors.toList()); + assertEquals(names, expectedNames, + "Unexpected results for query: " + query + " Expected: " + expectedNames + " but got " + names); + return response; + } + + /** Convenience overload using AFTS. */ + protected SearchResponse testSearchQueryUnordered(String query, Set expectedNames) + { + return testSearchQueryUnordered(query, expectedNames, SearchLanguage.AFTS); + } + + /** Runs a search and asserts that the returned entry names equal {@code expectedNames} as a set (order ignored). */ + protected SearchResponse testSearchQueryUnordered(String query, Set expectedNames, SearchLanguage queryLanguage) + { + SearchResponse response = performSearch(testUser, query, queryLanguage, getDefaultPagingOptions()); + Set names = response.getEntries().stream() + .map(s -> s.getModel().getName()) + .collect(Collectors.toSet()); + assertEquals(names, expectedNames, "Unexpected results for query: " + query); + return response; + } + + protected SearchResponse performSearch(UserModel asUser, String query, SearchLanguage queryLanguage, Pagination paging) + { + RestRequestQueryModel queryModel = new RestRequestQueryModel(); + queryModel.setQuery(query); + + UserModel user = ofNullable(asUser).orElse(testUser); + if (queryLanguage != null) + { + queryModel.setLanguage(queryLanguage.toString()); + } + return queryAsUser(user, queryModel, paging); + } + + private Pagination getDefaultPagingOptions() + { + Pagination paging = new Pagination(); + paging.setSkipCount(0); + paging.setMaxItems(100); + return paging; + } + + /** Builds a pagination object from optional {@code skipCount} / {@code maxItems}. */ + @SuppressWarnings("PMD.LinguisticNaming") + protected Pagination setPaging(Integer skipCount, Integer maxItems) + { + Pagination paging = new Pagination(); + if (skipCount != null) + { + paging.setSkipCount(skipCount); + } + if (maxItems != null) + { + paging.setMaxItems(maxItems); + } + return paging; + } + + /** + * Polls a CMIS query until {@code totalItems >= expectedCount} or {@link #SEARCH_MAX_ATTEMPTS} attempts have elapsed. Useful when a test needs to wait for the CMIS index to catch up to AFTS name-based indexing (the two can have different lag). + * + * @param asUser + * user to run the query as + * @param cmisQuery + * a CMIS query string (e.g. {@code "select * from cmis:document where IN_FOLDER('')"}) + * @param expectedCount + * the minimum {@code totalItems} required before returning + * @return {@code true} if the expected count was reached, {@code false} if all attempts were exhausted + */ + public boolean waitForCmisSearchCount(UserModel asUser, String cmisQuery, int expectedCount) + { + for (int attempt = 0; attempt < SEARCH_MAX_ATTEMPTS; attempt++) + { + SearchResponse response = performSearch(asUser, cmisQuery, SearchLanguage.CMIS, setPaging(0, 1000)); + if (response != null && !response.isEmpty() + && response.getPagination().getTotalItems() >= expectedCount) + { + return true; + } + Utility.waitToLoopTime(properties.getSolrWaitTimeInSeconds(), + "Waiting for CMIS folder index. Attempt: " + (attempt + 1)); + } + return false; + } + + // ------------------------------------------------------------------------- + // Spellcheck helpers + // ------------------------------------------------------------------------- + + /** + * Builds and runs a spellcheck-enabled search request: + * + *

+     * {
+     *   "query":      { "query": "<query>",     "userQuery": "<userQuery>" },
+     *   "spellcheck": { "query": "<userQuery>" }
+     * }
+     * 
+ * + * If {@code user} is {@code null}, {@link #testUser} is used. + */ + @SuppressWarnings("PMD.MethodNamingConventions") + protected SearchResponse SearchSpellcheckQuery(UserModel user, String query, String userQuery) + { + RestRequestSpellcheckModel spellCheck = new RestRequestSpellcheckModel(); + spellCheck.setQuery(userQuery); + + UserModel searchUser = ofNullable(user).orElse(testUser); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery(query); + queryReq.setUserQuery(userQuery); + return queryAsUser(searchUser, queryReq, spellCheck); + } + + /** + * Asserts the {@code spellCheck} object on a search response context: + * + */ + @SuppressWarnings("PMD.UnitTestShouldUseTestAnnotation") + public void testSearchSpellcheckResponse(SearchResponse response, String spellCheckType, String spellCheckSuggestion) + { + if (spellCheckType != null) + { + response.getContext().assertThat().field("spellCheck").isNotEmpty(); + response.getContext().getSpellCheck().assertThat().field("type").is(spellCheckType); + } + else + { + response.getContext().assertThat().field("spellCheck").isNull(); + } + + if (spellCheckSuggestion != null) + { + response.getContext().assertThat().field("spellCheck").isNotEmpty(); + response.getContext().getSpellCheck().assertThat().field("suggestions").contains(spellCheckSuggestion); + } + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/AbstractSearchExactTermTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/AbstractSearchExactTermTest.java new file mode 100644 index 0000000000..119c78b471 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/AbstractSearchExactTermTest.java @@ -0,0 +1,156 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import static java.util.Arrays.asList; +import static java.util.stream.IntStream.range; + +import static com.google.common.collect.ImmutableMap.of; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.chemistry.opencmis.commons.PropertyIds; +import org.apache.chemistry.opencmis.commons.enums.VersioningState; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; + +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.data.DataSite; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.FolderModel; + +/** + * Base corpus for Exact Term tests. Creates 5 documents with known name/title/description/content values used to verify exact term search behavior against standard cm:* properties. + */ +@SuppressWarnings("PMD.SimpleDateFormatNeedsLocale") +public abstract class AbstractSearchExactTermTest extends AbstractSearchServicesE2ETest +{ + @Autowired + protected DataSite dataSite; + + @Autowired + protected DataContent dataContent; + + private static final DateFormat QUERY_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); + protected String fromDate; + protected String toDate; + + private void prepareExactSearchData(FolderModel testFolder) + { + List> exactSearchData = asList( + of("name", "Running", + "description", "Running is a sport is a nice activity", + "content", "when you are running you are doing an amazing sport", + "title", "Running jumping"), + of("name", "Run", + "description", "you are supposed to run jump", + "content", "after many runs you are tired and if you jump it happens the same", + "title", "Run : a philosophy"), + of("name", "Poetry", + "description", "a document about poetry and jumpers", + "content", "poetry is unrelated to sport", + "title", "Running jumping twice jumpers"), + of("name", "Jump", + "description", "a document about jumps", + "content", "runnings jumpings", + "title", "Running"), + of("name", "Running jumping", + "description", "runners jumpers runs everywhere", + "content", "run is Good as jump", + "title", "Running the art of jumping")); + + List createdFileModels = new ArrayList<>(); + range(0, exactSearchData.size()).forEach(id -> { + Map record = exactSearchData.get(id); + + Map properties = new HashMap<>(); + properties.put(PropertyIds.OBJECT_TYPE_ID, "D:tok:document"); + properties.put(PropertyIds.NAME, record.get("name")); + properties.put("cm:title", record.get("title")); + properties.put("cm:description", record.get("description")); + properties.put("tok:true", record.get("title")); // tokenized copy + properties.put("tok:false", record.get("title")); // non-tokenized copy + properties.put("tok:both", record.get("title")); // both copy + properties.put(PropertyIds.SECONDARY_OBJECT_TYPE_IDS, List.of("P:cm:titled")); + + FileModel fileModel = FileModel.getRandomFileModel(FileType.TEXT_PLAIN, record.get("content")); + fileModel.setName(record.get("name")); + + cmisApi.authenticateUser(testUser).usingSite(testSite).usingResource(testFolder) + .createFile(fileModel, properties, VersioningState.MAJOR) + .assertThat().existsInRepo(); + + createdFileModels.add(fileModel); + }); + + waitForContentIndexing("Good", true); + } + + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + serverHealth.assertServerIsOnline(); + + deployCustomModel("tokenised-model.xml"); + + FolderModel testFolder = dataContent.usingSite(testSite).usingUser(testUser).createFolder(); + prepareExactSearchData(testFolder); + + // date range calculation + Date today = new Date(); + LocalDateTime yesterday = today.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().plusDays(-1); + fromDate = QUERY_DATE_FORMAT.format(Date.from(yesterday.atZone(ZoneId.systemDefault()).toInstant())); + LocalDateTime tomorrow = today.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().plusDays(1); + toDate = QUERY_DATE_FORMAT.format(Date.from(tomorrow.atZone(ZoneId.systemDefault()).toInstant())); + } + + protected void assertResponseCardinality(String query, int num) + { + SearchResponse response = queryAsUser(testUser, query); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), num, query); + } + + protected void assertException(String query) + { + queryAsUser(testUser, query); + Assert.assertTrue( + restClient.getStatusCode().equals(String.valueOf(HttpStatus.NOT_IMPLEMENTED.value())) || + restClient.getStatusCode().equals(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value())), + "Status code is not as expected."); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/AbstractSearchServicesE2ETest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/AbstractSearchServicesE2ETest.java new file mode 100644 index 0000000000..72a7b7f26c --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/AbstractSearchServicesE2ETest.java @@ -0,0 +1,161 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import static java.util.List.of; + +import org.springframework.http.HttpStatus; +import org.testng.Assert; + +import org.alfresco.rest.exception.EmptyJsonResponseException; +import org.alfresco.utility.Utility; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.UserModel; + +/** + * Abstract Search test class that contains useful methods such as: + *
    + *
  • Preparing the data to index.
  • + *
  • Polling the search API until results are present.
  • + *
+ *

+ * Mirrors the InsightEngine {@code AbstractSearchServicesE2ETest}: a thin specialisation of {@link AbstractE2EFunctionalTest} that adds the standard "search services" sample data set. All Spring context, autowires, common test user/site setup and generic search/query/wait/ spellcheck helpers are inherited from {@link AbstractE2EFunctionalTest}. + */ +@SuppressWarnings({"PMD.LongVariable", "PMD.OneDeclarationPerLine"}) +public abstract class AbstractSearchServicesE2ETest extends AbstractE2EFunctionalTest +{ + private static final String SEARCH_DATA_SAMPLE_FOLDER = "FolderSearch"; + + private static final int MAX_ATTEMPTS_TO_RETRY_QUERY = 10; + private static final int MAX_WAIT_IN_SECONDS_BEFORE_RETRY_QUERY = 5; + private static final int MAX_ATTEMPTS_TO_READ_RESPONSE = 10; + private static final int MAX_WAIT_IN_SECONDS_BEFORE_REREAD_RESPONSE = 2; + + protected FileModel file, file2, file3, file4; + protected FolderModel folder; + + /** + * Creates the standard test data structure used across search tests: + * + *

+     * |- FolderSearch
+     * |-- pangram.txt
+     * |-- cars.PDF
+     * |-- alfresco.docx
+     * |-- <uniqueFileName>.ODT
+     * 
+ */ + public void searchServicesDataPreparation() + { + folder = new FolderModel(SEARCH_DATA_SAMPLE_FOLDER); + dataContent.usingUser(testUser).usingSite(testSite).createFolder(folder); + + String title = "Title: " + unique_searchString; + String description = "Description: File is created for search tests by Author: " + unique_searchString + " . "; + + file = new FileModel("pangram.txt", "pangram" + title, description, FileType.TEXT_PLAIN, + description + " The quick brown fox jumps over the lazy dog"); + + file2 = new FileModel("cars.PDF", "cars", description, FileType.PDF, + "The landrover discovery is not a sports car"); + + file3 = new FileModel("alfresco.docx", "alfresco", "alfresco", FileType.TEXT_PLAIN, + "Alfresco text file for search "); + + file4 = new FileModel(unique_searchString + ".ODT", "uniquee" + title, description, FileType.TEXT_PLAIN, + "Unique text file for search "); + + of(file, file2, file3, file4).forEach( + f -> dataContent.usingUser(testUser).usingSite(testSite).usingResource(folder).createContent(f)); + + waitForMetadataIndexing(file.getName(), true); + waitForMetadataIndexing(file2.getName(), true); + waitForMetadataIndexing(file3.getName(), true); + waitForMetadataIndexing(file4.getName(), true); + } + + /** + * Creates a file with the provided text in cm:name, cm:title, cm:description and cm:content (under {@link #folder} of {@link #testSite}) and waits for the content to be indexed. + */ + protected FileModel createFileWithProvidedText(String filename, String providedText) throws InterruptedException + { + String title = "Title: File containing " + providedText; + String description = "Description: Contains provided string: " + providedText; + FileModel uniqueFile = new FileModel(filename, title, description, FileType.TEXT_PLAIN, + "The content " + providedText + " is a provided string"); + dataContent.usingUser(testUser).usingSite(testSite).usingResource(folder).createContent(uniqueFile); + Assert.assertTrue(waitForContentIndexing(providedText, true)); + return uniqueFile; + } + + /** + * Repeats the query until the response status is OK and the entries list is non-empty, up to {@link #MAX_ATTEMPTS_TO_READ_RESPONSE} re-reads after a successful status. + */ + protected SearchResponse queryUntilResponseEntriesListNotEmpty(UserModel user, String queryString) + { + SearchResponse response = queryUntilStatusIsOk(user, queryString); + if (restClient.getStatusCode().matches(String.valueOf(HttpStatus.OK.value()))) + { + for (int readAttempts = 0; readAttempts < MAX_ATTEMPTS_TO_READ_RESPONSE; readAttempts++) + { + if (!response.isEmpty()) + { + return response; + } + Utility.waitToLoopTime(MAX_WAIT_IN_SECONDS_BEFORE_REREAD_RESPONSE, + "Re-reading empty response. Retry Attempt: " + (readAttempts + 1)); + } + } + return response; + } + + private SearchResponse queryUntilStatusIsOk(UserModel user, String queryString) + { + // Repeat query until status is OK or query retry limit is hit + for (int queryAttempts = 0; queryAttempts < MAX_ATTEMPTS_TO_RETRY_QUERY - 1; queryAttempts++) + { + try + { + SearchResponse response = queryAsUser(user, queryString); + if (restClient.getStatusCode().matches(String.valueOf(HttpStatus.OK.value()))) + { + return response; + } + Utility.waitToLoopTime(MAX_WAIT_IN_SECONDS_BEFORE_RETRY_QUERY, + "Re-trying query for valid status code. Retry Attempt: " + (queryAttempts + 1)); + } + catch (EmptyJsonResponseException ignore) + { + // try again + } + } + // Final attempt + return queryAsUser(user, queryString); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/ExplicitRoutingTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/ExplicitRoutingTest.java new file mode 100644 index 0000000000..5445c23267 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/ExplicitRoutingTest.java @@ -0,0 +1,97 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import static org.testng.Assert.assertTrue; + +import java.util.List; +import java.util.Map; + +import org.apache.chemistry.opencmis.commons.PropertyIds; +import org.apache.chemistry.opencmis.commons.enums.VersioningState; +import org.testng.annotations.Test; + +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; + +public class ExplicitRoutingTest extends AbstractE2EFunctionalTest +{ + /** + * Checks indexing still works after sharding model used for explicit routing has been disabled + */ + @Test(priority = 1) + public void testIndexingStillWorkingAfterShardModelIsDeactivated() + { + + // Deploy sharding model + assertTrue(deployCustomModel("sharding-content-model.xml"), + "failing while deploying sharding model"); + + // Create a first child in parent folder. It will be indexed in the parent shard (shard 0) + FileModel file = FileModel.getRandomFileModel(FileType.TEXT_PLAIN, "custom content"); + Map propertiesFirstChild = Map.of(PropertyIds.NAME, file.getName(), + PropertyIds.OBJECT_TYPE_ID, "cmis:document", + "cmis:secondaryObjectTypeIds", List.of("P:shard:sharding"), + "shard:shardId", "0"); + + // Create file using shard:shardId (explicit routing) + cmisApi.authenticateUser(testUser).usingSite(testSite) + .createFile(file, propertiesFirstChild, VersioningState.MAJOR) + .assertThat().existsInRepo(); + + // Wait for file to be indexed + assertTrue(isContentInSearchResults(file.getName(), file.getName(), true), + "A file using sharding model has not been indexed"); + + // Deleting file + dataContent.usingSite(testSite).usingUser(testUser).usingResource(file).deleteContent(); + restClient.withCoreAPI().usingTrashcan().deleteNodeFromTrashcan(file); + + // Deleting sharding model + assertTrue(deactivateCustomModel("sharding-content-model.xml"), + "failing while deactivating sharding model"); + assertTrue(deleteCustomModel("sharding-content-model.xml"), + "failing while removing sharding model"); + + assertTrue(waitForIndexing("TYPE:'" + "shard:shardId" + "'", false), + "Indexes are not updated after deactivating a model"); + + // Create a file in the parent folder + file = FileModel.getRandomFileModel(FileType.TEXT_PLAIN, "custom content"); + + cmisApi.authenticateUser(testUser).usingSite(testSite) + .createFile(file, + Map.of(PropertyIds.NAME, file.getName(), + PropertyIds.OBJECT_TYPE_ID, "cmis:document"), + VersioningState.MAJOR) + .assertThat().existsInRepo(); + + assertTrue(isContentInSearchResults(file.getName(), file.getName(), true), + "Indexing is not working after the sharding model has been removed"); + + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/FacetFieldsSearchTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/FacetFieldsSearchTest.java new file mode 100644 index 0000000000..eda976f9bb --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/FacetFieldsSearchTest.java @@ -0,0 +1,253 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import java.util.ArrayList; +import java.util.List; +import jakarta.json.Json; +import jakarta.json.JsonObject; + +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.dataprep.SiteService.Visibility; +import org.alfresco.utility.data.RandomData; +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.UserModel; + +/** + * Faceted search test with FacetFields + */ +@SuppressWarnings("PMD.OneDeclarationPerLine") +public class FacetFieldsSearchTest extends AbstractSearchServicesE2ETest +{ + private UserModel userWithNoAccess, userCanAccessTextFile; + private SiteModel testSite; + private String fname; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + serverHealth.assertServerIsOnline(); + + fname = unique_searchString + "facet"; + + testSite = new SiteModel(RandomData.getRandomName("SiteSearch")); + testSite.setVisibility(Visibility.PRIVATE); + + testSite = dataSite.usingUser(testUser).createSite(testSite); + + // Create another user who would not have access to the Private Site created by testUser + userWithNoAccess = dataUser.createRandomTestUser("UserSearch2"); + userCanAccessTextFile = dataUser.createRandomTestUser("UserSearch3"); + + // Create a folder and a file as test User + FolderModel testFolder = new FolderModel(fname); + dataContent.usingUser(testUser).usingSite(testSite).createFolder(testFolder); + + FileModel textFile = new FileModel(fname + "-1.txt", fname, fname, FileType.TEXT_PLAIN, fname + " file for search "); + dataContent.usingUser(testUser).usingSite(testSite).createContent(textFile); + + FileModel htmlFile = new FileModel(fname + "-2.html", FileType.HTML, fname + " file 2 for search "); + dataContent.usingUser(testUser).usingSite(testSite).createContent(htmlFile); + + // Set Node Permissions to allow access for user for text File + JsonObject userPermission = Json.createObjectBuilder() + .add("permissions", Json.createObjectBuilder().add("isInheritanceEnabled", false) + .add("locallySet", Json.createObjectBuilder().add("authorityId", userCanAccessTextFile.getUsername()) + .add("name", "SiteConsumer").add("accessStatus", "ALLOWED"))) + .build(); + String putBody = userPermission.toString(); + + restClient.authenticateUser(testUser).withCoreAPI().usingNode(textFile).updateNode(putBody); + + // Wait for the file to be indexed + waitForContentIndexing(htmlFile.getContent(), true); + } + + @Test + public void testSearchFacetFieldsBucketExcludedWhenMinCount2() + { + // Create Query with FacetFields: Site and Content MimeType + SearchRequest query = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("name:" + fname); + query.setQuery(queryReq); + + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List facets = new ArrayList<>(); + + facets.add(new RestRequestFacetFieldModel("SITE", "SEARCH.FACET_FIELDS.SITE", 0)); // MinCount = 0 + facets.add(new RestRequestFacetFieldModel("cm:content.mimetype", "Mimetype", 2)); // MinCount = 2 + + facetFields.setFacets(facets); + query.setFacetFields(facetFields); + + // Search query using user who created site + SearchResponse response = query(query); + + // Expect MimeType bucket with size 1 is excluded as minCount = 2 won't be reached + Assert.assertEquals(response.getContext().getFacetsFields().size(), 1); + + // Expect SITE bucket is included and MinCount defaults to 1 + RestResultBucketsModel facetFieldList = response.getContext().getFacetsFields().getFirst(); + Assert.assertEquals(facetFieldList.getLabel(), "SEARCH.FACET_FIELDS.SITE"); + Assert.assertEquals(facetFieldList.getBuckets().size(), 1); + + FacetFieldBucket bucket1 = facetFieldList.getBuckets().getFirst(); + bucket1.assertThat().field("label").is(testSite.getId()); + bucket1.assertThat().field("filterQuery").contains(testSite.getId()); + bucket1.assertThat().field("count").is(3); // One folder and 2 files created above + } + + @Test + public void testSearchWithFacetFieldsMinCountChecks() + { + SearchRequest query = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("name:" + fname); + query.setQuery(queryReq); + + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List facets = new ArrayList<>(); + + // MinCount not set + facets.add(new RestRequestFacetFieldModel("SITE", "SEARCH.FACET_FIELD1.SITE", null)); // MinCount Not set + facets.add(new RestRequestFacetFieldModel("cm:content.mimetype", "SEARCH.FACET_FIELD2.Mimetype", 1)); // MinCount = 1 + + facetFields.setFacets(facets); + query.setFacetFields(facetFields); + + // Search query using user who created site + SearchResponse response = query(query); + + // Expect 2 Facet buckets to be retrieved: MinCount when not set defaults to 1 + List facetFieldBucketsList = response.getContext().getFacetsFields(); + Assert.assertEquals(facetFieldBucketsList.size(), 2, "FacetField"); + + RestResultBucketsModel facetFieldList = facetFieldBucketsList.getFirst(); + Assert.assertEquals(facetFieldList.getLabel(), "SEARCH.FACET_FIELD1.SITE"); + + FacetFieldBucket bucket1 = facetFieldList.getBuckets().getFirst(); + bucket1.assertThat().field("label").is(testSite.getId()); + bucket1.assertThat().field("filterQuery").contains(testSite.getId()); + bucket1.assertThat().field("count").is(3); + + // MimeType bucket will be shown with 2 buckets + facetFieldList = facetFieldBucketsList.get(1); + Assert.assertEquals(facetFieldList.getLabel(), "SEARCH.FACET_FIELD2.Mimetype"); + Assert.assertEquals(facetFieldList.getBuckets().size(), 2); + + bucket1 = facetFieldList.getBuckets().getFirst(); + bucket1.assertThat().field("label").is("text/html"); + bucket1.assertThat().field("filterQuery").contains("text/html"); + bucket1.assertThat().field("count").is(1); + bucket1.assertThat().field("display").is("HTML"); + + bucket1 = facetFieldList.getBuckets().get(1); + bucket1.assertThat().field("label").is("text/plain"); + bucket1.assertThat().field("filterQuery").contains("text/plain"); + bucket1.assertThat().field("count").is(1); + bucket1.assertThat().field("display").is("Plain Text"); + } + + @Test + public void testSearchWithFacetFieldsNoFacetsWhenNoAccess() + { + SearchRequest query = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("name:" + fname); + query.setQuery(queryReq); + + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List facets = new ArrayList<>(); + + facets.add(new RestRequestFacetFieldModel("SITE", "SEARCH.FACET_FIELD1.SITE", null)); // MinCount Not set + facets.add(new RestRequestFacetFieldModel("cm:content.mimetype", "SEARCH.FACET_FIELD2.Mimetype", 1)); // MinCount = 1 + + facetFields.setFacets(facets); + query.setFacetFields(facetFields); + + // Search query using other user + SearchResponse response = restClient.authenticateUser(userWithNoAccess).withSearchAPI().search(query); + + // User has No access to matching content hence no buckets expected + Assert.assertNull(response.getContext().getFacetsFields()); + } + + @Test + public void testSearchWithFacetFieldsOnlyFacetsWhereAccess() + { + SearchRequest query = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("name:" + fname); + query.setQuery(queryReq); + + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List facets = new ArrayList<>(); + + facets.add(new RestRequestFacetFieldModel("SITE", "SEARCH.FACET_FIELD1.SITE", null)); // MinCount Not set + facets.add(new RestRequestFacetFieldModel("cm:content.mimetype", "SEARCH.FACET_FIELD2.Mimetype", 1)); // MinCount = 1 + + facetFields.setFacets(facets); + query.setFacetFields(facetFields); + + // Search query using user that can access 1 text file + SearchResponse response = restClient.authenticateUser(userCanAccessTextFile).withSearchAPI().search(query); + + List facetFieldBucketsList = response.getContext().getFacetsFields(); + Assert.assertEquals(facetFieldBucketsList.size(), 2); + + // Check FacetField 1 + RestResultBucketsModel facetFieldList = facetFieldBucketsList.getFirst(); + + // User has granular permissions to content within this private site, so expect the Site bucket + Assert.assertEquals(facetFieldList.getLabel(), "SEARCH.FACET_FIELD1.SITE"); + + FacetFieldBucket bucket1 = facetFieldList.getBuckets().getFirst(); + bucket1.assertThat().field("label").is(testSite.getId()); + bucket1.assertThat().field("filterQuery").contains(testSite.getId()); + bucket1.assertThat().field("count").is(1); + + // Expect MimeType bucket shows only 1 bucket where user has access to content + facetFieldList = facetFieldBucketsList.get(1); + Assert.assertEquals(facetFieldList.getLabel(), "SEARCH.FACET_FIELD2.Mimetype"); + Assert.assertEquals(facetFieldList.getBuckets().size(), 1); + + // User has access to text file alone, so expect bucket for text/plain and not for html content. + bucket1 = facetFieldList.getBuckets().getFirst(); + bucket1.assertThat().field("label").is("text/plain"); + bucket1.assertThat().field("label").isNot("text/html"); + bucket1.assertThat().field("filterQuery").contains("text/plain"); + bucket1.assertThat().field("count").is(1); + bucket1.assertThat().field("display").is("Plain Text"); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/FacetIntervalSearchTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/FacetIntervalSearchTest.java new file mode 100644 index 0000000000..0b7ab5eb12 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/FacetIntervalSearchTest.java @@ -0,0 +1,186 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import java.util.Arrays; +import java.util.Collections; + +import org.springframework.http.HttpStatus; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.model.RestErrorModel; + +/** + * Faceted Intervals Search Test + */ +public class FacetIntervalSearchTest extends AbstractSearchServicesE2ETest +{ + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + searchServicesDataPreparation(); + waitForContentIndexing(file4.getContent(), true); + } + + @Test + public void checkingFacetsMandatoryErrorMessages() + { + SearchRequest query = createQuery("cars"); + + RestRequestFacetIntervalsModel facetIntervalsModel = new RestRequestFacetIntervalsModel(); + FacetInterval facetInterval = new FacetInterval(null, null, null); + facetIntervalsModel.setIntervals(Collections.singletonList(facetInterval)); + query.setFacetIntervals(facetIntervalsModel); + + query(query); + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError() + .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "facetIntervals intervals field")); + facetInterval.setField("created"); + query(query); + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError() + .containsSummary(String.format(RestErrorModel.MANDATORY_COLLECTION, "facetIntervals intervals sets")); + + RestRequestFacetSetModel restFacetSetModel = new RestRequestFacetSetModel(); + restFacetSetModel.setLabel("theRest"); + facetInterval.setSets(Collections.singletonList(restFacetSetModel)); + query(query); + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError() + .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "facetIntervals intervals created sets start")); + + restFacetSetModel.setStart("A"); + query(query); + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError() + .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "facetIntervals intervals created sets end")); + + restFacetSetModel.setEnd("B"); + RestRequestFacetSetModel duplicate = new RestRequestFacetSetModel(); + facetInterval.setSets(Arrays.asList(restFacetSetModel, duplicate)); + duplicate.setLabel("theRest"); + duplicate.setStart("A"); + duplicate.setEnd("C"); + + query(query); + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError() + .containsSummary("duplicate set interval label [theRest=2]"); + + facetInterval.setSets(Collections.singletonList(restFacetSetModel)); + facetInterval.setLabel("thesame"); + FacetInterval duplicateLabel = new FacetInterval("creator", "thesame", Collections.singletonList(duplicate)); + facetIntervalsModel.setIntervals(Arrays.asList(facetInterval, duplicateLabel)); + query.setFacetIntervals(facetIntervalsModel); + + query(query); + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError() + .containsSummary("duplicate interval label [thesame=2]"); + } + + @Test + public void searchWithBasicInterval() + { + SearchRequest query = createQuery("cars"); + + RestRequestFacetIntervalsModel facetIntervalsModel = new RestRequestFacetIntervalsModel(); + RestRequestFacetSetModel restRequestFacetSetModel = new RestRequestFacetSetModel(); + restRequestFacetSetModel.setStart("a"); + restRequestFacetSetModel.setEnd("user"); + restRequestFacetSetModel.setLabel("aUser"); + + RestRequestFacetSetModel restFacetSetModel = new RestRequestFacetSetModel(); + restFacetSetModel.setStart("user"); + restFacetSetModel.setEnd("z"); + restFacetSetModel.setStartInclusive(false); + restFacetSetModel.setLabel("theRest"); + + FacetInterval facetInterval = new FacetInterval("creator", null, Arrays.asList(restRequestFacetSetModel, restFacetSetModel)); + facetIntervalsModel.setIntervals(Collections.singletonList(facetInterval)); + query.setFacetIntervals(facetIntervalsModel); + + SearchResponse response = query(query); + response.assertThat().entriesListIsNotEmpty(); + response.getContext().assertThat().field("facets").isNotEmpty(); + RestGenericFacetResponseModel facetResponseModel = response.getContext().getFacets().getFirst(); + + RestGenericBucketModel bucket = facetResponseModel.getBuckets().getFirst(); + Assert.assertEquals(facetResponseModel.getBuckets().size(), 2); + bucket.assertThat().field("label").is("aUser"); + bucket.assertThat().field("filterQuery").is("creator:[\"a\" TO \"user\"]"); + bucket.getMetrics().getFirst().assertThat().field("type").is("count"); + bucket.getMetrics().getFirst().assertThat().field("value").contains("{count="); + + bucket = facetResponseModel.getBuckets().get(1); + + bucket.assertThat().field("label").is("theRest"); + bucket.assertThat().field("filterQuery").is("creator:[\"user\" TO \"z\"]"); + bucket.getMetrics().getFirst().assertThat().field("type").is("count"); + bucket.getMetrics().getFirst().assertThat().field("value").is("{count=0}"); + } + + @Test + public void searchWithDates() + { + SearchRequest query = createQuery("cars"); + + RestRequestFacetIntervalsModel facetIntervalsModel = new RestRequestFacetIntervalsModel(); + RestRequestFacetSetModel restRequestFacetSetModel = new RestRequestFacetSetModel(); + restRequestFacetSetModel.setStart("*"); + restRequestFacetSetModel.setEnd("2016"); + restRequestFacetSetModel.setEndInclusive(false); + restRequestFacetSetModel.setLabel("Before2016"); + + RestRequestFacetSetModel restFacetSetModel = new RestRequestFacetSetModel(); + restFacetSetModel.setStart("2016"); + restFacetSetModel.setEnd("now"); + restFacetSetModel.setLabel("From2016"); + + FacetInterval facetInterval = new FacetInterval("cm:modified", "modified", Arrays.asList(restRequestFacetSetModel, restFacetSetModel)); + facetIntervalsModel.setIntervals(Collections.singletonList(facetInterval)); + query.setFacetIntervals(facetIntervalsModel); + + SearchResponse response = query(query); + response.assertThat().entriesListIsNotEmpty(); + response.getContext().assertThat().field("facets").isNotEmpty(); + RestGenericFacetResponseModel facetResponseModel = response.getContext().getFacets().getFirst(); + + facetResponseModel.assertThat().field("label").is("modified"); + RestGenericBucketModel bucket = facetResponseModel.getBuckets().getFirst(); + Assert.assertEquals(facetResponseModel.getBuckets().size(), 2); + + bucket.assertThat().field("label").is("From2016"); + bucket.assertThat().field("filterQuery").is("cm:modified:[\"2016\" TO \"now\"]"); + bucket.getMetrics().getFirst().assertThat().field("type").is("count"); + bucket.getMetrics().getFirst().assertThat().field("value").contains("{count="); + + bucket = facetResponseModel.getBuckets().get(1); + + bucket.assertThat().field("label").is("Before2016"); + bucket.assertThat().field("filterQuery").is("cm:modified:[\"*\" TO \"2016\"]"); + bucket.getMetrics().getFirst().assertThat().field("type").is("count"); + bucket.getMetrics().getFirst().assertThat().field("value").is("{count=0}"); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/FacetRangeSearchTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/FacetRangeSearchTest.java new file mode 100644 index 0000000000..aec16ba312 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/FacetRangeSearchTest.java @@ -0,0 +1,336 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.springframework.http.HttpStatus; +import org.testng.SkipException; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.model.RestErrorModel; +import org.alfresco.rest.model.RestRequestRangesModel; +import org.alfresco.utility.LogFactory; + +/** + * Faceted Range Search Query for numeric range { "query": { "query": "name:A*" }, "range": { "field": "content.size", "start": "0", "end": "400", "gap": "100" } } Date range query: { "query": { "query": "name:A*" }, "range": { "field": "created", "start": "2015-09-29T10:45:15.729Z", "end": "2016-09-29T10:45:15.729Z", "gap": "+100DAY" } } + * + */ +public class FacetRangeSearchTest extends AbstractSearchServicesE2ETest +{ + private static final Logger LOGGER = LogFactory.getLogger(); + + /** AFTS predicate that swsdp content satisfies (swsdp ships documents with cm:created in 2011–2015). */ + private static final String SAMPLE_2015_DOC_QUERY = "created:[\"2015-09-29T10:45:15.729Z\" TO \"2016-09-29T10:45:15.729Z\"]"; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + searchServicesDataPreparation(); + waitForContentIndexing(file4.getContent(), true); + } + + /** Check the error messages mention the mandatory fields when they are omitted. */ + @Test + public void checkingFacetsMandatoryErrorMessages() + { + SearchRequest query = createQuery("cars"); + + // Omit the field. + query.setRanges(List.of(createRangesModel(null, "0", "400", "20"))); + query(query); + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError() + .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "field")); + + // Omit the start. + query.setRanges(List.of(createRangesModel("content.size", null, "400", "20"))); + query(query); + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError() + .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "start")); + + // Omit the end. + query.setRanges(List.of(createRangesModel("content.size", "0", null, "20"))); + query(query); + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError() + .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "end")); + + // Omit the gap. + query.setRanges(List.of(createRangesModel("content.size", "0", "400", null))); + query(query); + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError() + .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "gap")); + } + + @Test + @SuppressWarnings("unchecked") + public void searchWithRange() + { + SearchRequest query = createQuery("* AND SITE:'" + testSite.getId() + "'"); + + RestRequestRangesModel facetRangeModel = createRangesModel("content.size", "0", "200", "20"); + List ranges = List.of(facetRangeModel); + query.setRanges(ranges); + SearchResponse response = query(query); + response.assertThat().entriesListIsNotEmpty(); + response.getContext().assertThat().field("facets").isNotEmpty(); + RestGenericFacetResponseModel facetResponseModel = response.getContext().getFacets().getFirst(); + + RestGenericBucketModel bucket = facetResponseModel.getBuckets().getFirst(); + bucket.assertThat().field("label").is("[20 - 40)"); + bucket.assertThat().field("filterQuery").is("content.size:[\"20\" TO \"40\">"); + Map metric = (Map) bucket.getMetrics().getFirst().getValue(); + assertEquals(Integer.valueOf(metric.get("count")).intValue(), 2, "Unexpected count for first bucket."); + Map info = (Map) bucket.getBucketInfo(); + assertEquals(info.get("start"), "20"); + assertEquals(info.get("end"), "40"); + assertNull(info.get("count")); + assertEquals(info.get("startInclusive"), "true"); + assertEquals(info.get("endInclusive"), "false"); + + bucket = facetResponseModel.getBuckets().get(1); + bucket.assertThat().field("label").is("[40 - 120)"); + bucket.assertThat().field("filterQuery").is("content.size:[\"40\" TO \"120\">"); + metric = (Map) bucket.getMetrics().getFirst().getValue(); + assertEquals(Integer.valueOf(metric.get("count")).intValue(), 1, "Unexpected count for second bucket."); + info = (Map) bucket.getBucketInfo(); + assertEquals(info.get("start"), "40"); + assertEquals(info.get("end"), "120"); + assertEquals(info.get("startInclusive"), "true"); + assertEquals(info.get("endInclusive"), "false"); + + bucket = facetResponseModel.getBuckets().get(2); + bucket.assertThat().field("label").is("[120 - 200]"); + bucket.assertThat().field("filterQuery").is("content.size:[\"120\" TO \"200\"]"); + assertEquals(Integer.valueOf(metric.get("count")).intValue(), 1, "Unexpected count for third bucket."); + info = (Map) bucket.getBucketInfo(); + assertEquals(info.get("start"), "120"); + assertEquals(info.get("end"), "200"); + assertEquals(info.get("startInclusive"), "true"); + assertEquals(info.get("endInclusive"), "true"); + } + + @Test + @SuppressWarnings("unchecked") + public void searchWithRangeHardend() + { + SearchRequest query = createQuery("* AND SITE:'" + testSite.getId() + "'"); + + RestRequestRangesModel facetRangeModel = createRangesModel("content.size", "0", "200", "20"); + facetRangeModel.setHardend(true); + List ranges = List.of(facetRangeModel); + query.setRanges(ranges); + SearchResponse response = query(query); + response.assertThat().entriesListIsNotEmpty(); + response.getContext().assertThat().field("facets").isNotEmpty(); + RestGenericFacetResponseModel facetResponseModel = response.getContext().getFacets().getFirst(); + + RestGenericBucketModel bucket = facetResponseModel.getBuckets().getFirst(); + bucket.assertThat().field("label").is("[20 - 40)"); + bucket.assertThat().field("filterQuery").is("content.size:[\"20\" TO \"40\">"); + Map metric = (Map) bucket.getMetrics().getFirst().getValue(); + assertEquals(Integer.valueOf(metric.get("count")).intValue(), 2, "Unexpected count for first bucket."); + Map info = (Map) bucket.getBucketInfo(); + assertEquals(info.get("start"), "20"); + assertEquals(info.get("end"), "40"); + assertEquals(info.get("startInclusive"), "true"); + assertEquals(info.get("endInclusive"), "false"); + assertNull(info.get("count")); + + bucket = facetResponseModel.getBuckets().get(1); + bucket.assertThat().field("label").is("[40 - 120)"); + bucket.assertThat().field("filterQuery").is("content.size:[\"40\" TO \"120\">"); + info = (Map) bucket.getBucketInfo(); + assertEquals(info.get("start"), "40"); + assertEquals(info.get("end"), "120"); + metric = (Map) bucket.getMetrics().getFirst().getValue(); + assertEquals(Integer.valueOf(metric.get("count")).intValue(), 1, "Unexpected count for second bucket."); + assertNull(info.get("count")); + assertEquals(info.get("startInclusive"), "true"); + assertEquals(info.get("endInclusive"), "false"); + + bucket = facetResponseModel.getBuckets().get(2); + bucket.assertThat().field("label").is("[120 - 200]"); + bucket.assertThat().field("filterQuery").is("content.size:[\"120\" TO \"200\"]"); + metric = (Map) bucket.getMetrics().getFirst().getValue(); + assertEquals(Integer.valueOf(metric.get("count")).intValue(), 1, "Unexpected count for third bucket."); + info = (Map) bucket.getBucketInfo(); + assertEquals(info.get("start"), "120"); + assertEquals(info.get("end"), "200"); + assertNull(info.get("count")); + assertEquals(info.get("startInclusive"), "true"); + assertEquals(info.get("endInclusive"), "true"); + } + + /** This test relies on a document created in 2015 existing, probably part of the sample site. */ + @Test + @SuppressWarnings("unchecked") + public void searchDateRange() + { + skipIfSwsdp2015DocMissing(); + + SearchRequest query = createQuery("name:A*"); + + RestRequestRangesModel facetRangeModel = createRangesModel("created", "2015-09-29T10:45:15.729Z", "2016-09-29T10:45:15.729Z", "+280DAY"); + List ranges = List.of(facetRangeModel); + query.setRanges(ranges); + SearchResponse response = query(query); + response.assertThat().entriesListIsNotEmpty(); + response.getContext().assertThat().field("facets").isNotEmpty(); + RestGenericFacetResponseModel facetResponseModel = response.getContext().getFacets().getFirst(); + + List buckets = facetResponseModel.getBuckets(); + assertThat(buckets.size(), is(1)); + + RestGenericBucketModel bucket = buckets.getFirst(); + bucket.assertThat().field("label").is("[2015-09-29T10:45:15.729Z - 2017-04-11T10:45:15.729Z]"); + bucket.assertThat().field("filterQuery").is("created:[\"2015-09-29T10:45:15.729Z\" TO \"2017-04-11T10:45:15.729Z\"]"); + bucket.getMetrics().getFirst().assertThat().field("value").is("{count=1}"); + Map info = (Map) bucket.getBucketInfo(); + assertEquals(info.get("start"), "2015-09-29T10:45:15.729Z"); + assertEquals(info.get("end"), "2017-04-11T10:45:15.729Z"); + assertNull(info.get("count"), "1"); + assertEquals(info.get("startInclusive"), "true"); + assertEquals(info.get("endInclusive"), "true"); + } + + @Test + @SuppressWarnings("PMD.UnitTestShouldIncludeAssert") + public void searchDateAndSizeRanges() + { + SearchRequest query = createQuery("* AND SITE:'" + testSite.getId() + "'"); + RestRequestRangesModel facetRangeModel = createRangesModel("created", "2015-09-29T10:45:15.729Z", "2016-09-29T10:45:15.729Z", "+280DAY"); + RestRequestRangesModel facetCountRangeModel = createRangesModel("content.size", "0", "500", "200"); + List ranges = List.of(facetRangeModel, facetCountRangeModel); + query.setRanges(ranges); + } + + @Test + @SuppressWarnings("unchecked") + public void searchWithRangeAndIncludeUpperBound() + { + SearchRequest query = createQuery("* AND SITE:'" + testSite.getId() + "'"); + + RestRequestRangesModel facetRangeModel = createRangesModel("content.size", "0", "200", "20"); + List include = List.of("upper"); + facetRangeModel.setInclude(include); + List ranges = List.of(facetRangeModel); + query.setRanges(ranges); + SearchResponse response = query(query); + response.assertThat().entriesListIsNotEmpty(); + response.getContext().assertThat().field("facets").isNotEmpty(); + RestGenericFacetResponseModel facetResponseModel = response.getContext().getFacets().getFirst(); + + RestGenericBucketModel bucket = facetResponseModel.getBuckets().getFirst(); + bucket.assertThat().field("label").is("(20 - 40]"); + bucket.assertThat().field("filterQuery").is("content.size:<\"20\" TO \"40\"]"); + Map metric = (Map) bucket.getMetrics().getFirst().getValue(); + assertEquals(Integer.valueOf(metric.get("count")).intValue(), 2, "Unexpected count for first bucket."); + Map info = (Map) bucket.getBucketInfo(); + assertEquals(info.get("start"), "20"); + assertEquals(info.get("end"), "40"); + assertNull(info.get("count")); + assertEquals(info.get("startInclusive"), "false"); + assertEquals(info.get("endInclusive"), "true"); + + bucket = facetResponseModel.getBuckets().get(1); + bucket.assertThat().field("label").is("(40 - 120]"); + bucket.assertThat().field("filterQuery").is("content.size:<\"40\" TO \"120\"]"); + metric = (Map) bucket.getMetrics().getFirst().getValue(); + assertEquals(Integer.valueOf(metric.get("count")).intValue(), 1, "Unexpected count for second bucket."); + info = (Map) bucket.getBucketInfo(); + assertEquals(info.get("start"), "40"); + assertEquals(info.get("end"), "120"); + assertEquals(info.get("startInclusive"), "false"); + assertEquals(info.get("endInclusive"), "true"); + + bucket = facetResponseModel.getBuckets().get(2); + bucket.assertThat().field("label").is("(120 - 200]"); + bucket.assertThat().field("filterQuery").is("content.size:<\"120\" TO \"200\"]"); + metric = (Map) bucket.getMetrics().getFirst().getValue(); + assertEquals(Integer.valueOf(metric.get("count")).intValue(), 1, "Unexpected count for third bucket."); + info = (Map) bucket.getBucketInfo(); + assertEquals(info.get("start"), "120"); + assertEquals(info.get("end"), "200"); + assertEquals(info.get("startInclusive"), "false"); + assertEquals(info.get("endInclusive"), "true"); + } + + /** + * Create a ranges model with the values given. + * + * @param field + * The field to facet on. + * @param start + * The lowest facet value. + * @param end + * The highest facet value. + * @param gap + * The size of the buckets. + * @return The facet ranges model. + */ + private RestRequestRangesModel createRangesModel(String field, String start, String end, String gap) + { + RestRequestRangesModel facetRangeModel = new RestRequestRangesModel(); + facetRangeModel.setField(field); + facetRangeModel.setStart(start); + facetRangeModel.setEnd(end); + facetRangeModel.setGap(gap); + return facetRangeModel; + } + + /** + * Skips the calling test (via {@link SkipException}) when the repository under test has no document with {@code cm:created} in the 2015-09-29 → 2016-09-29 window. That document is only present when the {@code alfresco-share-services} AMP bootstrapped the SWSDP sample site (see {@code patch.siteLoadPatch.swsdp} in {@code share-services/module-context.xml}). + * + *

+ * The AMP is bundled by the {@code acs-packaging} Docker image ({@code alfresco/alfresco-content-repository}), but NOT by the base images used in the {@code alfresco-community-repo} and {@code alfresco-enterprise-repo} minimal test stacks. Therefore this date-range facet test is meant to run end-to-end only as part of an {@code acs-packaging} validation; when executed against community/enterprise alone it is skipped instead of failing with an NPE. + */ + private void skipIfSwsdp2015DocMissing() + { + SearchResponse precheck = queryAsUser(dataUser.getAdminUser(), SAMPLE_2015_DOC_QUERY); + boolean hasSample = precheck != null + && precheck.getPagination() != null + && precheck.getPagination().getTotalItems() != null + && precheck.getPagination().getTotalItems() > 0; + if (!hasSample) + { + String reason = "Skipping searchDateRange: no document with cm:created in 2015-09-29 → 2016-09-29 " + + "was found. The SWSDP sample site (shipped by the alfresco-share-services AMP via " + + "acs-packaging) is not present on this deployment, so the date-range facet cannot be " + + "exercised. This test is intended to be run as part of acs-packaging validation."; + LOGGER.warn(reason); + throw new SkipException(reason); + } + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/FacetedSearchTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/FacetedSearchTest.java new file mode 100644 index 0000000000..c34b6bbcfb --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/FacetedSearchTest.java @@ -0,0 +1,307 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.chemistry.opencmis.commons.PropertyIds; +import org.apache.chemistry.opencmis.commons.enums.VersioningState; +import org.testng.Assert; +import org.testng.TestException; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; + +/** + * Faceted search test. + */ +public class FacetedSearchTest extends AbstractSearchServicesE2ETest +{ + /** + * Perform the below facet query. { "query": { "query": "cars", "language": "afts" }, "facetQueries": [ {"query": "content.size:[o TO 102400]", "label": "small"}, {"query": "content.size:[102400 TO 1048576]", "label": "medium"}, {"query": "content.size:[1048576 TO 16777216]", "label": "large"} ], "facetFields": {"facets": [{"field": "'content.size'"}]} } + * + * Expected response {"list": { "entries": [... All the results], "pagination": { "maxItems": 100, "hasMoreItems": false, "totalItems": 61, "count": 61, "skipCount": 0 }, "facetsFields": [ { "type": "query", "label": "foo", "buckets": [ { "label": "small", "filterQuery": "content.size:[0 TO 102400]", "display": 1 }, { "label": "large", "filterQuery": "content.size:[1048576 TO 16777216]", "metrics": [ { "type": "count", "value": { "count": 0 } } ] }, }} + */ + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + searchServicesDataPreparation(); + waitForContentIndexing(file4.getContent(), true); + } + + @Test + public void searchWithQueryFaceting() + { + SearchRequest query = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("cars"); + query.setQuery(queryReq); + + List facets = new ArrayList<>(); + facets.add(new FacetQuery("content.size:[0 TO 102400]", "small")); + facets.add(new FacetQuery("content.size:[102400 TO 1048576]", "medium")); + facets.add(new FacetQuery("content.size:[1048576 TO 16777216]", "large")); + query.setFacetQueries(facets); + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List list = new ArrayList<>(); + list.add(new RestRequestFacetFieldModel("'content.size'")); + facetFields.setFacets(list); + query.setFacetFields(facetFields); + query.setIncludeRequest(true); + + SearchResponse response = query(query); + + response.assertThat().entriesListIsNotEmpty(); + response.getContext().assertThat().field("facetQueries").isNotEmpty(); + + FacetFieldBucket facet = response.getContext().getFacetQueries().getFirst(); + facet.assertThat().field("label").contains("small").and().field("count").isGreaterThan(0); + facet.assertThat().field("label").contains("small").and().field("filterQuery").is("content.size:[0 TO 102400]"); + Assert.assertEquals(response.getContext().getFacetQueries().size(), 1, "Results with count=0 must be omitted"); + + // We don't expect to see the FacetFields if group is being used. + Assert.assertNull(response.getContext().getFacetsFields()); + Assert.assertNull(response.getContext().getFacets()); + } + + /** + * Verify this query is returning the same results for both single server and shard environments. + */ + @Test + @SuppressWarnings("PMD.UnitTestShouldIncludeAssert") + public void searchWithQueryFacetingCluster() + { + searchWithQueryFaceting(); + } + + /** + * * Perform a group by faceting, below test groups the facet by group name foo. { "query": { "query": "cars", "language": "afts" }, "facetQueries": [ {"query": "content.size:[o TO 102400]", "label": "small","group":"foo"}, {"query": "content.size:[102400 TO 1048576]", "label": "medium","group":"foo"}, {"query": "content.size:[1048576 TO 16777216]", "label": "large","group":"foo"} ], "facetFields": {"facets": [{"field": "'content.size'"}]} } + * + * Expected response {"list": { "entries": [... All the results], "pagination": { "maxItems": 100, "hasMoreItems": false, "totalItems": 61, "count": 61, "skipCount": 0 }, "context": { "consistency": {"lastTxId": 512}, //Added below as part of SEARCH-374 "facets": [ { "label": "foo", "buckets": [ { "label": "small", "count": 61, "filterQuery": "content.size:[o TO 102400]"}, { "label": "large", "count": 0, "filterQuery": "content.size:[1048576 TO 16777216]"}, { "label": "medium", "count": 61, "filterQuery": "content.size:[102400 TO 1048576]"} ] } } }} + */ + @Test + public void searchQueryFacetingWithGroup() + { + SearchRequest query = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("cars"); + query.setQuery(queryReq); + + List facets = new ArrayList<>(); + facets.add(new FacetQuery("content.size:[0 TO 102400]", "small", "foo")); + facets.add(new FacetQuery("content.size:[102400 TO 1048576]", "medium", "foo")); + facets.add(new FacetQuery("content.size:[1048576 TO 16777216]", "large", "foo")); + query.setFacetQueries(facets); + + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List list = new ArrayList<>(); + list.add(new RestRequestFacetFieldModel("'content.size'")); + facetFields.setFacets(list); + query.setFacetFields(facetFields); + + SearchResponse response = query(query); + + // We don't expect to see the FacetQueries if group is being used. + Assert.assertNull(response.getContext().getFacetQueries()); + // Validate the facet field structure is correct. + Assert.assertFalse(response.getContext().getFacets().isEmpty()); + Assert.assertEquals(response.getContext().getFacets().getFirst().getLabel(), "foo"); + + RestGenericBucketModel bucket = response.getContext().getFacets().getFirst().getBuckets().getFirst(); + bucket.assertThat().field("label").isNotEmpty(); + Assert.assertEquals(bucket.getMetrics().getFirst().getType(), "count"); + Assert.assertNotEquals(bucket.getMetrics().getFirst().getValue(), ""); + bucket.assertThat().field("filterQuery").isNotEmpty(); + response.getContext().getFacets().getFirst().getBuckets().forEach(action -> { + switch (action.getLabel()) + { + case "small": + Assert.assertEquals(action.getFilterQuery(), "content.size:[0 TO 102400]"); + break; + case "medium": + Assert.assertEquals(action.getFilterQuery(), "content.size:[102400 TO 1048576]"); + break; + case "large": + Assert.assertEquals(action.getFilterQuery(), "content.size:[1048576 TO 16777216]"); + break; + + default: + throw new TestException("Unexpected value returned"); + } + }); + } + + /** + * { "query": { "query": "*" }, "facetFields": { "facets": [{"field": "cm:mimetype"},{"field": "modifier"}] } } + */ + @Test + public void searchWithFactedFields() + { + SearchRequest query = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("cm:content:" + unique_searchString); + query.setQuery(queryReq); + + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List facets = new ArrayList<>(); + facets.add(new RestRequestFacetFieldModel("cm:mimetype")); + facets.add(new RestRequestFacetFieldModel("modifier")); + facetFields.setFacets(facets); + query.setFacetFields(facetFields); + + SearchResponse response = query(query); + + Assert.assertFalse(response.getContext().getFacetsFields().isEmpty()); + Assert.assertNull(response.getContext().getFacetQueries()); + Assert.assertNull(response.getContext().getFacets()); + + RestResultBucketsModel model = response.getContext().getFacetsFields().getFirst(); + Assert.assertEquals(model.getLabel(), "modifier"); + + model.assertThat().field("label").is("modifier"); + FacetFieldBucket bucket1 = model.getBuckets().getFirst(); + bucket1.assertThat().field("label").is(testUser.getUsername()); + bucket1.assertThat().field("display").is("FN-" + testUser.getUsername() + " LN-" + testUser.getUsername()); + bucket1.assertThat().field("filterQuery").is("modifier:\"" + testUser.getUsername() + "\""); + bucket1.assertThat().field("count").is(1); + } + + /** + * Test that items returned are in the format of generic facets. { "query": { "query": "*" }, "facetFields": { "facets": [{"field": "cm:mimetype"},{"field": "modifier"}] }, "facetFormat":"V2" } + */ + @Test + public void searchWithFactedFieldsFacetFormatV2() + { + SearchRequest query = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("cm:content:" + unique_searchString); + query.setQuery(queryReq); + query.setFacetFormat("V2"); + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List facets = new ArrayList<>(); + facets.add(new RestRequestFacetFieldModel("cm:mimetype")); + facets.add(new RestRequestFacetFieldModel("modifier")); + facetFields.setFacets(facets); + query.setFacetFields(facetFields); + + SearchResponse response = query(query); + + Assert.assertNull(response.getContext().getFacetsFields()); + Assert.assertNull(response.getContext().getFacetQueries()); + Assert.assertFalse(response.getContext().getFacets().isEmpty()); + RestGenericFacetResponseModel model = response.getContext().getFacets().getFirst(); + Assert.assertEquals(model.getLabel(), "modifier"); + + model.assertThat().field("label").is("modifier"); + RestGenericBucketModel bucket1 = model.getBuckets().getFirst(); + bucket1.assertThat().field("label").is(testUser.getUsername()); + bucket1.assertThat().field("display").is("FN-" + testUser.getUsername() + " LN-" + testUser.getUsername()); + bucket1.assertThat().field("filterQuery").is("modifier:\"" + testUser.getUsername() + "\""); + bucket1.assertThat().field("metrics.entry").is("[null]") + .and().field("metrics.type").is("[count]") + .and().field("metrics.value").is("[{count=1}]"); + } + + /** + * Test that facet fields return results for single and multivalued fields. { "query": { "query": "cm:addressee:'first'" }, "facetFields": { "facets": [{"field": "cm:addressee"}, {"field": "cm:addressees"}] }, "facetFormat":"V2" } + */ + @Test + public void searchWithMultiValuedFieldsFacet() + { + // Create properties with single (cm:addressee) and multivalued (cm:addressees) values + FileModel emailFile = FileModel.getRandomFileModel(FileType.TEXT_PLAIN, "Email"); + + Map properties = new HashMap<>(); + properties.put(PropertyIds.OBJECT_TYPE_ID, "cmis:document"); + properties.put(PropertyIds.NAME, emailFile.getName()); + properties.put(PropertyIds.SECONDARY_OBJECT_TYPE_IDS, List.of("P:cm:emailed")); + properties.put("cm:addressee", "first"); + properties.put("cm:addressees", List.of("first", "second")); + + cmisApi.authenticateUser(testUser) + .usingSite(testSite) + .usingResource(folder) + .createFile(emailFile, properties, VersioningState.MAJOR) + .assertThat().existsInRepo(); + + String addresseeQuery = "cm:addressee:'first'"; + Assert.assertTrue(waitForIndexing(addresseeQuery, true)); + + // Search facets fields cm:addressee and cm:addressess + SearchRequest query = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("cm:addressee:'first'"); + query.setQuery(queryReq); + query.setFacetFormat("V2"); + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List facets = new ArrayList<>(); + facets.add(new RestRequestFacetFieldModel("cm:addressee")); + facets.add(new RestRequestFacetFieldModel("cm:addressees")); + facetFields.setFacets(facets); + query.setFacetFields(facetFields); + + SearchResponse response = query(query); + + // Verify results + Assert.assertNull(response.getContext().getFacetsFields()); + Assert.assertNull(response.getContext().getFacetQueries()); + Assert.assertFalse(response.getContext().getFacets().isEmpty()); + + // Facets for cm:addressees (multivalued) + RestGenericFacetResponseModel model = response.getContext().getFacets().getFirst(); + Assert.assertEquals(model.getLabel(), "cm:addressees"); + model.assertThat().field("label").is("cm:addressees"); + RestGenericBucketModel bucket = model.getBuckets().getFirst(); + bucket.assertThat().field("label").is("{en}first"); + bucket.assertThat().field("filterQuery").is("cm:addressees:\"{en}first\""); + bucket.assertThat().field("metrics.entry").is("[null]") + .and().field("metrics.type").is("[count]") + .and().field("metrics.value").is("[{count=1}]"); + bucket = model.getBuckets().get(1); + bucket.assertThat().field("label").is("{en}second"); + bucket.assertThat().field("filterQuery").is("cm:addressees:\"{en}second\""); + bucket.assertThat().field("metrics.entry").is("[null]") + .and().field("metrics.type").is("[count]") + .and().field("metrics.value").is("[{count=1}]"); + + // Facets for cm:addressee (single valued) + model = response.getContext().getFacets().get(1); + Assert.assertEquals(model.getLabel(), "cm:addressee"); + model.assertThat().field("label").is("cm:addressee"); + bucket = model.getBuckets().getFirst(); + bucket.assertThat().field("label").is("{en}first"); + bucket.assertThat().field("filterQuery").is("cm:addressee:\"{en}first\""); + bucket.assertThat().field("metrics.entry").is("[null]") + .and().field("metrics.type").is("[count]") + .and().field("metrics.value").is("[{count=1}]"); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/FieldDefinitionTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/FieldDefinitionTest.java new file mode 100644 index 0000000000..0cd788e668 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/FieldDefinitionTest.java @@ -0,0 +1,236 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.utility.data.CustomObjectTypeProperties; +import org.alfresco.utility.model.FileModel; + +/** + * A test for fields defined in the solr schema Based on ... + */ +@SuppressWarnings({"PMD.SingularField", "PMD.FieldNamingConventions", "PMD.OneDeclarationPerLine", "PMD.UseDiamondOperator"}) +public class FieldDefinitionTest extends AbstractSearchServicesE2ETest +{ + + private FileModel File1, File2, File3; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + dataContent.usingAdmin().deployContentModel("search-2139-model.xml"); + + File1 = new FileModel("standard-file1.txt"); + + dataContent.usingUser(testUser).usingSite(testSite).createCustomContent(File1, "cmis:document", new CustomObjectTypeProperties()); + + cmisApi.authenticateUser(testUser).usingResource(File1).addSecondaryTypes("P:allfieldtypes:text") + .updateProperty("allfieldtypes:mltextLOVPartial", "file1") + .updateProperty("allfieldtypes:textPatternUnique", "file2") + .updateProperty("allfieldtypes:mltextFree", "file3") + .updateProperty("allfieldtypes:textLOVPartial", "file4"); + + File2 = new FileModel("standard-file2.txt"); + + dataContent.usingUser(testUser).usingSite(testSite).createCustomContent(File2, "cmis:document", new CustomObjectTypeProperties()); + + cmisApi.authenticateUser(testUser).usingResource(File2).addSecondaryTypes("P:allfieldtypes:text") + .updateProperty("allfieldtypes:textFree", "text field definition test") + .updateProperty("allfieldtypes:textPatternMany", "mltext field definition test") + .updateProperty("allfieldtypes:textLOVWhole", "text field not tokenised") + .updateProperty("allfieldtypes:mltextLOVWhole", "mltext field not tokenised"); + + File3 = new FileModel("standard-file3.txt"); + + dataContent.usingUser(testUser).usingSite(testSite).createCustomContent(File3, "cmis:document", + new CustomObjectTypeProperties()); + + List mlMultipleValue = new ArrayList(); + mlMultipleValue.add("oranges"); + mlMultipleValue.add("apples"); + mlMultipleValue.add("pears"); + + cmisApi.authenticateUser(testUser).usingResource(File3).addSecondaryTypes("P:allfieldtypes:text") + .updateProperty("allfieldtypes:multiplemltext", mlMultipleValue); + + waitForMetadataIndexing(File1.getName(), true); + waitForMetadataIndexing(File2.getName(), true); + waitForMetadataIndexing(File3.getName(), true); + } + + // A test to test the text field in the solr schema, using a single word + @Test(priority = 1) + public void testTextField() + { + SearchResponse response = queryAsUser(testUser, "allfieldtypes_textPatternUnique:file2"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "allfieldtypes_textLOVPartial:file4"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + } + + // A test to test the mltext field in the solr schema, using a single word + @Test(priority = 2) + public void testmlTextField() + { + SearchResponse response = queryAsUser(testUser, "allfieldtypes_mltextLOVPartial:file1"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "allfieldtypes_mltextFree:file3"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + } + + // A test to test the text field in the solr schema, where the field hasn't been defined + @Test(priority = 3) + public void testTextFieldNotDefined() + { + SearchResponse response = queryAsUser(testUser, "allfieldtypes_textPatternMany:file1"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 0); + + response = queryAsUser(testUser, "allfieldtypes:textNone:file2"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 0); + } + + // A test to test the mltext field in the solr schema, where the field hasn't been defined + @Test(priority = 4) + public void testmlTextFieldNotDefined() + { + SearchResponse response = queryAsUser(testUser, "allfieldtypes_mltextNone:file1"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 0); + + response = queryAsUser(testUser, "allfieldtypes_mltextPatternUnique:file2"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 0); + } + + // A test to test the tokenised text field in the solr schema + @Test(priority = 5) + public void testTextFieldTokenised() + { + SearchResponse response = queryAsUser(testUser, "allfieldtypes_textFree:\"text field definition test\""); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "allfieldtypes_textFree:text"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "allfieldtypes_textFree:\"definition test\""); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "allfieldtypes_textPatternMany:definition"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + } + + // A test to test a non tokenised text field in the solr schema + @Test(priority = 6) + public void testTextFieldNotTokenised() + { + SearchResponse response = queryAsUser(testUser, "allfieldtypes_textLOVWhole:\"text field not tokenised\""); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "allfieldtypes_textLOVWhole:text"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 0); + + response = queryAsUser(testUser, "allfieldtypes_textLOVWhole:\"field not\""); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 0); + } + + // A test to test the tokenised mltext field in the solr schema + @Test(priority = 7) + public void testmlTextFieldTokenised() + { + SearchResponse response = queryAsUser(testUser, "allfieldtypes_textPatternMany:\"mltext field definition test\""); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "allfieldtypes_textPatternMany:mltext"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "allfieldtypes_textPatternMany:\"field definition\""); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "allfieldtypes_textPatternMany:field"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + } + + // A test to test the non tokenised mltext field in the solr schema + @Test(priority = 7) + public void testmlTextFieldNotTokenised() + { + SearchResponse response = queryAsUser(testUser, "allfieldtypes_mltextLOVWhole:\"mltext field not tokenised\""); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "allfieldtypes_mltextLOVWhole:mltext"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 0); + + response = queryAsUser(testUser, "allfieldtypes_mltextLOVWhole:\"not tokenised\""); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 0); + } + + // A test having multiple mltext field in the solr schema + @Test(priority = 8) + public void testmlTextFieldMuliple() + { + SearchResponse response = queryAsUser(testUser, "allfieldtypes_multiplemltext:\"orange\""); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "allfieldtypes_multiplemltext:\"apple\""); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "allfieldtypes_multiplemltext:\"pear\""); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/PivotFacetedSearchTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/PivotFacetedSearchTest.java new file mode 100644 index 0000000000..10e6e42c3f --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/PivotFacetedSearchTest.java @@ -0,0 +1,264 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.model.RestErrorModel; +import org.alfresco.rest.model.RestRequestRangesModel; + +/** + * Pivot Faceted search test. + */ +public class PivotFacetedSearchTest extends AbstractSearchServicesE2ETest +{ + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + searchServicesDataPreparation(); + waitForContentIndexing(file4.getContent(), true); + } + + @Test + public void searchWithPivotingErrors() + { + SearchRequest query = createQuery("cars"); + + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List list = new ArrayList<>(); + list.add(new RestRequestFacetFieldModel("'creator'")); + facetFields.setFacets(list); + query.setFacetFields(facetFields); + query.setIncludeRequest(false); + List pivotModelList = new ArrayList<>(); + RestRequestPivotModel pivots = new RestRequestPivotModel(); + pivotModelList.add(pivots); + query.setPivots(pivotModelList); + + query(query); + + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError() + .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "pivot key")); + + pivots.setKey("none_like_this"); + + query(query); + + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST) + .assertLastError().containsSummary("invalid argument was received") + .containsSummary("Pivot parameter none_like_this does not reference"); + } + + @Test + public void searchWithPivoting() + { + SearchRequest query = createQuery("cars"); + + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List list = new ArrayList<>(); + list.add(new RestRequestFacetFieldModel("creator")); + facetFields.setFacets(list); + query.setFacetFields(facetFields); + query.setIncludeRequest(false); + + SearchResponse response = query(query); + response.getContext().assertThat().field("facetsFields").isNotNull(); + + List pivotModelList = new ArrayList<>(); + RestRequestPivotModel pivots = new RestRequestPivotModel(); + pivots.setKey("creator"); + pivotModelList.add(pivots); + query.setPivots(pivotModelList); + response = query(query); + + // Pivot key has matched facet field so there is no longer a facet fields response + assertPivotResponse(response, "creator", null); + } + + @Test + public void searchWithNestedPivoting() + { + SearchRequest query = createQuery("cars"); + + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List list = new ArrayList<>(); + RestRequestFacetFieldModel restRequestFacetFieldModel = new RestRequestFacetFieldModel("SITE"); + restRequestFacetFieldModel.setLabel("site"); + list.add(restRequestFacetFieldModel); + list.add(new RestRequestFacetFieldModel("creator")); + list.add(new RestRequestFacetFieldModel("modifier")); + facetFields.setFacets(list); + query.setFacetFields(facetFields); + query.setIncludeRequest(false); + + List pivotModelList = new ArrayList<>(); + RestRequestPivotModel sitepivots = new RestRequestPivotModel(); + sitepivots.setKey("site"); + RestRequestPivotModel creatorpivot = new RestRequestPivotModel(); + creatorpivot.setKey("creator"); + RestRequestPivotModel modifierpivot = new RestRequestPivotModel(); + modifierpivot.setKey("modifier"); + sitepivots.setPivots(Arrays.asList(creatorpivot, modifierpivot)); + pivotModelList.add(sitepivots); + query.setPivots(pivotModelList); + + query(query); + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST) + .assertLastError().containsSummary("invalid argument was received") + .containsSummary("Currently only 1 nested pivot is supported, you have 2"); + + pivotModelList = new ArrayList<>(); + sitepivots = new RestRequestPivotModel(); + sitepivots.setKey("site"); + sitepivots.setPivots(Collections.singletonList(modifierpivot)); + pivotModelList.add(sitepivots); + pivotModelList.add(creatorpivot); + query.setPivots(pivotModelList); + + SearchResponse response = query(query); + assertPivotResponse(response, "SITE", "site"); + + RestGenericFacetResponseModel siteResponse = response.getContext().getFacets().getFirst(); + RestGenericBucketModel bucket = siteResponse.getBuckets().getFirst(); + RestGenericFacetResponseModel modifiedResponse = bucket.getFacets().getFirst(); + modifiedResponse.assertThat().field("type").is("pivot"); + modifiedResponse.assertThat().field("label").is("modifier"); + + RestGenericFacetResponseModel creatorResponse = response.getContext().getFacets().get(1); + creatorResponse.assertThat().field("type").is("pivot"); + creatorResponse.assertThat().field("label").is("creator"); + } + + @Test + public void searchWithRangePivoting() + { + SearchRequest query = createQuery("cars"); + + String endDate = LocalDateTime.now() + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")); + + Pagination pagination = new Pagination(); + pagination.setMaxItems(2); + query.setPaging(pagination); + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List list = new ArrayList<>(); + list.add(new RestRequestFacetFieldModel("creator")); + facetFields.setFacets(list); + query.setFacetFields(facetFields); + query.setIncludeRequest(false); + + RestRequestRangesModel facetRangeModel = new RestRequestRangesModel(); + facetRangeModel.setField("created"); + facetRangeModel.setStart("2015-09-29T10:45:15.729Z"); + facetRangeModel.setEnd(endDate); + facetRangeModel.setGap("+280DAY"); + facetRangeModel.setLabel("aRange"); + List ranges = new ArrayList<>(); + ranges.add(facetRangeModel); + query.setRanges(ranges); + + List pivotModelList = new ArrayList<>(); + RestRequestPivotModel creatorpivot = new RestRequestPivotModel(); + creatorpivot.setKey("creator"); + RestRequestPivotModel rangepivot = new RestRequestPivotModel(); + rangepivot.setKey("aRange"); + creatorpivot.setPivots(Collections.singletonList(rangepivot)); + pivotModelList.add(creatorpivot); + query.setPivots(pivotModelList); + + SearchResponse response = query(query); + RestGenericFacetResponseModel facetResponseModel = response.getContext().getFacets().get(1); + facetResponseModel.assertThat().field("type").is("pivot"); + facetResponseModel.assertThat().field("label").is("creator"); + RestGenericBucketModel bucket = facetResponseModel.getBuckets().getFirst(); + RestGenericFacetResponseModel rangeResponse = bucket.getFacets().getFirst(); + rangeResponse.assertThat().field("type").is("range"); + } + + private void assertPivotResponse(SearchResponse response, String field, String alabel) + { + String label = alabel != null ? alabel : field; + response.getContext().assertThat().field("facetsFields").isNull(); + response.getContext().assertThat().field("facets").isNotEmpty(); + RestGenericFacetResponseModel facetResponseModel = response.getContext().getFacets().getFirst(); + facetResponseModel.assertThat().field("type").is("pivot"); + facetResponseModel.assertThat().field("label").is(label); + RestGenericBucketModel bucket = facetResponseModel.getBuckets().getFirst(); + bucket.assertThat().field("label").isNotEmpty(); + bucket.assertThat().field("filterQuery").is(field + ":\"" + bucket.getLabel() + "\""); + Assert.assertEquals(bucket.getMetrics().getFirst().getType(), "count"); + Assert.assertTrue(bucket.getMetrics().getFirst().getValue().toString().contains("{count=")); + } + + @Test + public void searchWithPivotingUsingLabel() + { + SearchRequest query = createQuery("cars"); + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List list = new ArrayList<>(); + RestRequestFacetFieldModel creatorFacetFieldModel = new RestRequestFacetFieldModel("creator"); + creatorFacetFieldModel.setLabel("create"); + list.add(creatorFacetFieldModel); + RestRequestFacetFieldModel restRequestFacetFieldModel = new RestRequestFacetFieldModel("modifier"); + restRequestFacetFieldModel.setLabel("aLabel"); + list.add(restRequestFacetFieldModel); + facetFields.setFacets(list); + query.setFacetFields(facetFields); + query.setIncludeRequest(false); + RestRequestPivotModel pivots = new RestRequestPivotModel(); + pivots.setKey("create"); + RestRequestPivotModel pivotmod = new RestRequestPivotModel(); + pivotmod.setKey("aLabel"); + + List pivotModelList = new ArrayList<>(); + pivotModelList.add(pivots); + pivotModelList.add(pivotmod); + query.setPivots(pivotModelList); + SearchResponse response = query(query); + assertPivotResponse(response, "creator", "create"); + + // Now check the nesting + RestGenericFacetResponseModel labelResponseModel = response.getContext().getFacets().get(1); + labelResponseModel.assertThat().field("type").is("pivot"); + labelResponseModel.assertThat().field("label").isNotEmpty(); + labelResponseModel.assertThat().field("label").is("aLabel"); + RestGenericBucketModel bucket = labelResponseModel.getBuckets().getFirst(); + bucket.assertThat().field("filterQuery").is("modifier:\"" + bucket.getLabel() + "\""); + Assert.assertEquals(bucket.getMetrics().getFirst().getType(), "count"); + Assert.assertTrue(bucket.getMetrics().getFirst().getValue().toString().contains("{count=")); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchAFTSInFieldTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchAFTSInFieldTest.java new file mode 100644 index 0000000000..c10a0f172e --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchAFTSInFieldTest.java @@ -0,0 +1,361 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import static java.util.List.of; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.springframework.http.HttpStatus; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.exception.EmptyRestModelCollectionException; +import org.alfresco.rest.model.RestNodeAssociationModelCollection; +import org.alfresco.rest.model.RestNodeChildAssociationModel; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.FolderModel; + +/** + * Test class tests AFTS Search In Field works Created for Search-840 + */ +@SuppressWarnings({"PMD.OneDeclarationPerLine", "PMD.UnitTestShouldIncludeAssert"}) +public class SearchAFTSInFieldTest extends AbstractSearchServicesE2ETest +{ + private FolderModel folder1, folder2; + private FileModel file1, file2, file3, file4, file5; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + // Create Folders: + // Folder1: Expected to be found with file.txt + folder1 = new FolderModel("file txt folder"); + dataContent.usingUser(testUser).usingSite(testSite).createFolder(folder1); + + // Folder2: Not expected to be found with file.txt + folder2 = new FolderModel("txt files folder"); + dataContent.usingUser(testUser).usingSite(testSite).createFolder(folder2); + + // Create File(s): Expected to be found with file.txt + file1 = new FileModel("file.txt", "file.txt", "", FileType.TEXT_PLAIN, "file.txt"); + + file2 = new FileModel("1-file.txt", "1-file.txt", "", FileType.TEXT_PLAIN, "1-file.txt"); + + file3 = new FileModel("file1.txt", "file1.txt", "", FileType.TEXT_PLAIN, "file1.txt"); + + file4 = new FileModel("txt file", "txt file", "", FileType.TEXT_PLAIN, "txt file"); + + // Not Expected to be found with file.txt + file5 = new FileModel("txt files", "txt files", "", FileType.TEXT_PLAIN, "txt files"); + + of(file1, file2, file3, file4, file5).forEach( + f -> dataContent.usingUser(testUser).usingSite(testSite).usingResource(folder1).createContent(f)); + + waitForContentIndexing(file5.getContent(), true); + } + + @Test(priority = 1) + public void testSearchInFieldName() + { + // Field names in various formats + Stream fieldNames = Stream.of("{http://www.alfresco.org/model/content/1.0}name", + "@{http://www.alfresco.org/model/content/1.0}name", + "cm_name", + "cm:name", + "@cm:name", + "name"); + + // For each field name, check that queries return consistent results with / out '' + fieldNames.forEach(fieldName -> { + // Query string without quotes + String query = fieldName + ":file.txt"; + + Set expectedNames = new HashSet<>(); + expectedNames.add("file.txt"); // file1 + expectedNames.add("1-file.txt"); // file2 + expectedNames.add("file1.txt"); // file3 + expectedNames.add("txt file"); // file4 + expectedNames.add("file txt folder"); // folder1 + + testSearchQueryUnordered(query, expectedNames); + + // Query string in single quotes + query = fieldName + ":'file.txt'"; + testSearchQueryUnordered(query, expectedNames); + }); + } + + @Test(priority = 2) + public void testSearchInFieldTitle() + { + // Field names in various formats + Stream fieldNames = Stream.of("{http://www.alfresco.org/model/content/1.0}title", + "@{http://www.alfresco.org/model/content/1.0}title", + "cm_title", + "cm:title", + "@cm:title"); + + // For each field name, check that queries return consistent results with / out '' + fieldNames.forEach(fieldName -> { + String query = fieldName + ":" + file2.getName(); + boolean fileFound = isContentInSearchResults(query, file2.getName(), true); + Assert.assertTrue(fileFound, "File Not found for query: " + query); + + testSearchQuery(query, 1); + + query = fieldName + ":'" + file2.getName() + "\'"; + fileFound = isContentInSearchResults(query, file2.getName(), true); + Assert.assertTrue(fileFound, "File Not found for query: " + query); + + testSearchQuery(query, 1); + }); + } + + @Test(priority = 3) + public void testSearchInFieldContent() + { + // Field names in various formats + List fieldNames = new ArrayList<>(); + fieldNames.add("TEXT"); + fieldNames.add("{http://www.alfresco.org/model/dictionary/1.0}content"); + fieldNames.add("cm:content"); + fieldNames.add("d:content"); + + // For each field name, check that queries return consistent results with / out '' + fieldNames.forEach(fieldName -> { + String query = fieldName + ":" + file3.getContent(); + boolean fileFound = isContentInSearchResults(query, file3.getName(), true); + Assert.assertTrue(fileFound, "File Not found for query: " + query); + + Integer resultCount1 = testSearchQuery(query, null).getPagination().getTotalItems(); + + query = fieldName + ":'" + file3.getContent() + "\'"; + fileFound = isContentInSearchResults(query, file3.getName(), true); + Assert.assertTrue(fileFound, "File Not found for query: " + query); + + testSearchQuery(query, resultCount1).getPagination().getTotalItems(); + }); + } + + @Test + public void testSearchInFieldSITE() + { + String query = "SITE:" + testSite.getId(); + boolean fileFound = isContentInSearchResults(query, folder1.getName(), true); + Assert.assertTrue(fileFound, "Site Not found for query: " + query); + + Integer resultCount1 = testSearchQuery(query, 8, SearchLanguage.AFTS).getPagination().getTotalItems(); + + query = "SITE:'" + testSite.getId() + "\'"; + fileFound = isContentInSearchResults(query, folder1.getName(), true); + Assert.assertTrue(fileFound, "Site Not found for query: " + query); + + testSearchQuery(query, resultCount1, SearchLanguage.AFTS).getPagination().getTotalItems(); + } + + @Test(priority = 5) + public void testSearchInFieldTYPE() + { + // Field names in various formats + List fieldNames = new ArrayList<>(); + fieldNames.add("TYPE"); + fieldNames.add("EXACTTYPE"); + + // For each field name, check that queries return consistent results with / out '' + fieldNames.forEach(fieldName -> { + + String query = fieldName + ":cm\\:content" + " and =cm:name:" + file1.getName(); + boolean fileFound = isContentInSearchResults(query, file1.getName(), true); + Assert.assertTrue(fileFound, "Content Not found for query: " + query); + + Integer resultCount1 = testSearchQuery(query, 1).getPagination().getTotalItems(); + + query = fieldName + ":'cm:content'" + " and =cm:name:" + file1.getName(); + fileFound = isContentInSearchResults(query, file1.getName(), true); + Assert.assertTrue(fileFound, "Content Not found for query: " + query); + + testSearchQuery(query, resultCount1).getPagination().getTotalItems(); + }); + } + + @Test(priority = 6) + public void testSearchInFieldID() + { + String query = "ID:'workspace://SpacesStore/" + file1.getNodeRefWithoutVersion() + "'"; + boolean fileFound = isContentInSearchResults(query, file1.getName(), true); + Assert.assertTrue(fileFound, "Content Not found for query: " + query); + testSearchQuery(query, 1).getPagination().getTotalItems(); + } + + @Test(priority = 7) + public void testSearchInFieldPARENT() + { + String query = "PARENT:" + folder1.getNodeRefWithoutVersion(); + + Set expectedNames = new HashSet<>(); + expectedNames.add(file1.getName()); + expectedNames.add(file2.getName()); + expectedNames.add(file3.getName()); + expectedNames.add(file4.getName()); + expectedNames.add(file5.getName()); + + testSearchQueryUnordered(query, expectedNames); + query = "PARENT:'" + folder1.getNodeRefWithoutVersion() + "\'"; + testSearchQueryUnordered(query, expectedNames); + } + + @Test(priority = 8) + public void testSearchInFieldPRIMARYPARENT() throws EmptyRestModelCollectionException + { + // Create secondary association: file1 becomes a secondary child of folder2 + RestNodeChildAssociationModel childAssoc1 = new RestNodeChildAssociationModel(file1.getNodeRefWithoutVersion(), "cm:contains"); + restClient.authenticateUser(testUser).withCoreAPI().usingResource(folder2).addSecondaryChildren(childAssoc1); + + RestNodeAssociationModelCollection secondaryChildren = restClient.authenticateUser(testUser).withCoreAPI().usingResource(folder2).getSecondaryChildren(); + secondaryChildren.getEntryByIndex(0).assertThat().field("id").is(file1.getNodeRefWithoutVersion()); + + String query = "PRIMARYPARENT:'workspace://SpacesStore/" + folder1.getNodeRef() + "'"; + + Set expectedNames = new HashSet<>(); + expectedNames.add(file1.getName()); + expectedNames.add(file2.getName()); + expectedNames.add(file3.getName()); + expectedNames.add(file4.getName()); + expectedNames.add(file5.getName()); + + testSearchQueryUnordered(query, expectedNames, SearchLanguage.AFTS); + + query = "PARENT:'workspace://SpacesStore/" + folder2.getNodeRef() + "'"; + boolean fileFound = isContentInSearchResults(query, file1.getName(), true); + Assert.assertTrue(fileFound, "Expected Content Not found for query: " + query); + + testSearchQuery(query, 1, SearchLanguage.AFTS); + } + + @Test(priority = 9) + public void testSearchInFieldNameExactMatch() + { + // Check that queries return consistent results with / out '' + String query = "=name:" + file1.getName(); + boolean fileFound = isContentInSearchResults(query, file1.getName(), true); + Assert.assertTrue(fileFound, "File Not found for query: " + query); + + Integer resultCount1 = testSearchQuery(query, 1).getPagination().getTotalItems(); + Assert.assertSame(resultCount1, 1, "File count does not match for query: " + query); + + query = "=name:'" + file1.getName() + "\'"; + fileFound = isContentInSearchResults(query, file1.getName(), true); + Assert.assertTrue(fileFound, "File Not found for query: " + query); + + testSearchQuery(query, resultCount1).getPagination().getTotalItems(); + } + + @Test(priority = 10) + public void testSearchInFieldNameQueryExpansion() + { + // Check that queries return consistent results with / out '' + String query = "~name:" + file1.getName(); + + Set expectedNames = new HashSet<>(); + expectedNames.add(file1.getName()); + expectedNames.add(file2.getName()); + expectedNames.add(file3.getName()); + expectedNames.add(file4.getName()); + expectedNames.add(folder1.getName()); + + testSearchQueryUnordered(query, expectedNames); + + query = "~name:'" + file1.getName() + "\'"; + testSearchQueryUnordered(query, expectedNames); + } + + @Test(priority = 11) + public void testWithConjunctionDisjunctionAndNegation() + { + // Query string to include Conjunction, Disjunction and Negation + + String query1 = "~name:" + file1.getName(); // Query expected to return 5 results + String query2 = "=name:" + file2.getName(); // Query expected to return 1 result + String query3 = "=name:" + file3.getName(); // Query expected to return 1 result + + // Check Query with Conjunction, Negation, Disjunction: returns right results + // "~name:file.txt and ! (=name:1-file.txt or =name:file1.txt)" + String query = query1 + " and ! (" + query2 + " or " + query3 + ")"; + + // Check that expected files are included in the results + Set expectedNames = new HashSet<>(); + expectedNames.add("file.txt"); // file1 + expectedNames.add("txt file"); // file4 + expectedNames.add("file txt folder"); // folder1 + + SearchResponse response = testSearchQueryUnordered(query, expectedNames); + + // Check result count is 5-(1+1)=3 + int resultCount = response.getPagination().getTotalItems(); + Assert.assertEquals(resultCount, 3, "File count does not match for query: " + query); + + // Check that file2 and file3 are excluded from the results + boolean fileFound = isContentInSearchResponse(response, file2.getName()); + Assert.assertFalse(fileFound, "File2 found for query: " + query); + + fileFound = isContentInSearchResponse(response, file3.getName()); + Assert.assertFalse(fileFound, "File3 found for query: " + query); + } + + /** Check that a 501 error is returned when performing exact search on a tokenized field without cross-locale support. */ + @Test(priority = 12) + public void testExactMatchWithoutCrossLocale() + { + // Force the query to hit Solr rather than the DB. + String query = "=cm:title:test and cm:name:*"; + RestRequestQueryModel queryModel = new RestRequestQueryModel(); + queryModel.setQuery(query); + SearchRequest searchRequest = new SearchRequest(queryModel); + restClient.authenticateUser(dataUser.getAdminUser()).withSearchAPI().search(searchRequest); + restClient.assertStatusCodeIs(HttpStatus.NOT_IMPLEMENTED); + } + + /** Check that a 200 success is returned when performing exact search against the DB (even on a tokenized field without cross-locale support). */ + @Test(priority = 13) + public void testExactMatchAgainstDB() + { + // Using a simple query we will hit the DB. + String query = "=cm:title:test"; + RestRequestQueryModel queryModel = new RestRequestQueryModel(); + queryModel.setQuery(query); + SearchRequest searchRequest = new SearchRequest(queryModel); + restClient.authenticateUser(dataUser.getAdminUser()).withSearchAPI().search(searchRequest); + restClient.assertStatusCodeIs(HttpStatus.OK); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchAPATHTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchAPATHTest.java new file mode 100644 index 0000000000..e8a13db1a8 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchAPATHTest.java @@ -0,0 +1,163 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import java.util.Collections; +import java.util.List; + +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * Tests the search functionality using an ancestor path. Using the category as an example, it is a node that is located in root/rootCategory/classifiable. We now provide the ability to search by path so that we can return all the child elements of our target path. A search on root/rootCategory/classifiable should return Regions, Languages as they are the child elements of the given path. + * + */ + +public class SearchAPATHTest extends AbstractSearchServicesE2ETest +{ + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + searchServicesDataPreparation(); + waitForContentIndexing(file4.getContent(), true); + } + + /** + * { "query": { "query": "name:*" }, "facetFields": { "facets": [ {"field": "APATH", "prefix": "0" } ] } } Expected result entries[], "pagination": { "maxItems": 100, "hasMoreItems": true, "totalItems": 914, "count": 100, "skipCount": 0 }, "context": { "facetsFields": [{ "buckets": [ { "count": 913, "label": "0/5c09534f-3ca2-4272-bc25-064a7c1762b4" }, { "count": 2, "label": "0/" } ], "label": "APATH" }], "consistency": {"lastTxId": 89} } }} + * + */ + @Test + public void searchLevel0() + { + SearchRequest searchQuery = searchRequestWithAPATHFacet("name:*", "0"); + SearchResponse response = query(searchQuery); + + List buckets = getBuckets(response); + Assert.assertEquals(buckets.size(), 2); + + buckets.forEach(bucket -> bucket.assertThat().field("label").contains("0/")); + } + + /** + * Test to test that the facet buckets are returned correctly for sublevel 1/ Test to search for a searchString, that's unique to the test run and hence stable for any environment + */ + @Test + public void searchLevel0andIncludeSubLevel1() + { + SearchRequest searchQuery = searchRequestWithAPATHFacet("name:" + unique_searchString, "1/"); + SearchResponse response = query(searchQuery); + + List buckets = getBuckets(response); + Assert.assertEquals(buckets.size(), 1, "Incorrect bucket count"); + + getFirstBucket(response).assertThat().field("label").contains("1/"); + } + + @Test + public void searchLevel2() + { + String queryString = "name:cars"; + + SearchRequest l1Request = searchRequestWithAPATHFacet(queryString, "1/"); + SearchResponse l1Response = query(l1Request); + + String l2Prefix = getFirstBucket(l1Response).getLabel().replaceFirst("^1/", "2/"); + + SearchRequest l2Request = searchRequestWithAPATHFacet(queryString, l2Prefix); + SearchResponse l2Response = query(l2Request); + + FacetFieldBucket bucket = getFirstBucket(l2Response); + bucket.assertThat().field("label").contains(l2Prefix); + + String l3Prefix = bucket.getLabel().replaceFirst("^2/", "3/"); + + SearchRequest l3Request = searchRequestWithAPATHFacet(queryString, l3Prefix); + SearchResponse l3response = query(l3Request); + + List buckets = getBuckets(l3response); + Assert.assertEquals(buckets.size(), 1); + + getFirstBucket(l3response).assertThat().field("label").contains(l3Prefix); + } + + /** + * Creates a new {@link SearchRequest} for this test case. + * + * @param queryString + * the query string. + * @param facetPrefix + * the facet prefix. + * @return a new {@link SearchRequest} for this test case. + */ + private SearchRequest searchRequestWithAPATHFacet(String queryString, String facetPrefix) + { + SearchRequest searchRequest = new SearchRequest(); + + RestRequestQueryModel query = new RestRequestQueryModel(); + query.setQuery(queryString); + searchRequest.setQuery(query); + + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + facetFields.setFacets(Collections.singletonList(new RestRequestFacetFieldModel("APATH", facetPrefix))); + searchRequest.setFacetFields(facetFields); + + return searchRequest; + } + + /** + * Extracts the first bucket from the given response. + * + * @param response + * the results of a query execution. + * @return the first bucket included in the search response. + */ + private FacetFieldBucket getFirstBucket(SearchResponse response) + { + return getBuckets(response).iterator().next(); + } + + /** + * Extracts the buckets from the given response. The method also makes sure the buckets list is not empty in the input response. + * + * @param response + * the results of a query execution. + * @return the getBuckets included in the search response. + */ + private List getBuckets(SearchResponse response) + { + List facetFields = response.getContext().getFacetsFields(); + Assert.assertNotNull(facetFields); + Assert.assertFalse(facetFields.isEmpty()); + + List buckets = facetFields.iterator().next().getBuckets(); + Assert.assertNotNull(buckets); + Assert.assertFalse(buckets.isEmpty()); + + return buckets; + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchAspectTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchAspectTest.java new file mode 100644 index 0000000000..eff9277f6f --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchAspectTest.java @@ -0,0 +1,178 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.chemistry.opencmis.client.api.Document; +import org.apache.chemistry.opencmis.commons.PropertyIds; +import org.apache.chemistry.opencmis.commons.enums.VersioningState; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.utility.data.CustomObjectTypeProperties; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.FolderModel; + +/** + * Test class tests aspects are added and removed from Solr Documents Created for Search-2379 + */ + +public class SearchAspectTest extends AbstractSearchServicesE2ETest +{ + private FolderModel folder; + private FileModel file; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + folder = new FolderModel("folder-aspect"); + + file = new FileModel("file-aspect.txt"); + file.setContent("content file aspect"); + + dataContent.usingUser(testUser).usingSite(testSite).createCustomContent(folder, "cmis:folder", + new CustomObjectTypeProperties()); + + dataContent.usingUser(testUser).usingResource(folder).createCustomContent(file, "cmis:document", + new CustomObjectTypeProperties()); + + waitForMetadataIndexing(file.getName(), true); + + assertTrue(deployCustomModel("finance-model.xml"), + "failing while deploying model"); + } + + @Test(priority = 1) + public void testAspectIsRemoved() + { + // When checking out a file, cm:checkedOut aspect is added + cmisApi.authenticateUser(testUser).usingResource(file).checkOut(); + + String queryFile = "cm:name:'" + file.getName() + "'"; + + RestRequestQueryModel queryModel = new RestRequestQueryModel(); + queryModel.setQuery(queryFile); + queryModel.setLanguage(SearchLanguage.AFTS.toString()); + SearchRequest searchRequest = new SearchRequest(); + searchRequest.setQuery(queryModel); + searchRequest.setInclude(List.of("aspectNames")); + + SearchResponse response = restClient.authenticateUser(testUser).withSearchAPI().search(searchRequest); + assertTrue(response.getEntries().getFirst().getModel().getAspectNames().contains("cm:checkedOut"), + "checkedOut aspect expected"); + + // When cancelling the check out of a file, cm:checkedOut aspect is removed + cmisApi.authenticateUser(testUser).usingResource(file).cancelCheckOut(); + cmisApi.authenticateUser(testUser).usingResource(file).updateProperty(PropertyIds.NAME, + "file-aspect-random.txt"); + + waitForMetadataIndexing("file-aspect-random.txt", true); + + queryModel.setQuery("cm:name:'file-aspect-random.txt'"); + searchRequest.setQuery(queryModel); + response = restClient.authenticateUser(testUser).withSearchAPI().search(searchRequest); + + assertFalse(response.getEntries().getFirst().getModel().getAspectNames().contains("cm:checkedOut"), "checkedOut aspect was NOT expected"); + } + + /** + * Check that when an aspect is removed, all the properties defined in the aspect are removed as well. Created for Search-2538 + */ + @SuppressWarnings("unchecked") + @Test(priority = 2) + public void testAspectIsRemovedWithItsProperties() + { + String parkingLocationFieldName = "finance:ParkingLocation"; + String financeLocationFieldName = "finance:Location"; + + FileModel expenseLondon = FileModel.getRandomFileModel(FileType.TEXT_PLAIN, "Expense"); + + Map properties = new HashMap<>(); + properties.put(PropertyIds.OBJECT_TYPE_ID, "D:finance:Expense"); + properties.put(PropertyIds.NAME, expenseLondon.getName()); + properties.put(PropertyIds.SECONDARY_OBJECT_TYPE_IDS, List.of("P:finance:ParkEx")); + properties.put(financeLocationFieldName, "LondonBridge"); + properties.put(parkingLocationFieldName, "LiverpoolStreet"); + + // Compose query + String queryFile = "cm:name:'" + expenseLondon.getName() + "'"; + RestRequestQueryModel queryModel = new RestRequestQueryModel(); + queryModel.setQuery(queryFile); + queryModel.setLanguage(SearchLanguage.AFTS.toString()); + SearchRequest searchRequest = new SearchRequest(); + searchRequest.setQuery(queryModel); + searchRequest.setInclude(List.of("aspectNames", "properties")); + + cmisApi.authenticateUser(testUser) + .usingSite(testSite) + .usingResource(folder) + .createFile(expenseLondon, properties, VersioningState.MAJOR) + .assertThat().existsInRepo(); + + String parkingLocationQuery = parkingLocationFieldName + ":LiverpoolStreet"; + Assert.assertTrue(waitForIndexing(parkingLocationQuery, true)); + + // check that the document found has the expected properties and aspects defined. + SearchResponse response = restClient.authenticateUser(testUser).withSearchAPI().search(searchRequest); + assertTrue(response.getEntries().getFirst().getModel().getAspectNames().contains("finance:ParkEx"), + "parkEx aspect was expected"); + Map foundProperties = (Map) response.getEntries().getFirst().getModel().getProperties(); + assertEquals(foundProperties.get(financeLocationFieldName), "LondonBridge", + "finance:Location property is expected to be defined with 'LondonBridge' as value"); + assertEquals(foundProperties.get(parkingLocationFieldName), "LiverpoolStreet", + "finance:ParkingLocation property is expected to be defined with 'LiverpoolStreet' as value"); + + // remove aspect + Document doc = cmisApi.withCMISUtil().getCmisDocument(cmisApi.getLastResource()); + List aspects = doc.getProperty(PropertyIds.SECONDARY_OBJECT_TYPE_IDS).getValues(); + aspects.remove("P:finance:ParkEx"); + Map updateProperties = new HashMap<>(); + updateProperties.put(PropertyIds.SECONDARY_OBJECT_TYPE_IDS, aspects); + + doc.updateProperties(updateProperties); + + // Check that the field related to the removed aspect is no longer indexed for tested file + Assert.assertTrue(waitForIndexing(parkingLocationQuery, false)); + + response = restClient.authenticateUser(testUser).withSearchAPI().search(searchRequest); + assertFalse(response.getEntries().getFirst().getModel().getAspectNames().contains("finance:ParkEx"), "parkEx aspect was NOT expected"); + + // check that the document found has finance:Location property defined and finance:ParkingLocation has been removed + foundProperties = (Map) response.getEntries().getFirst().getModel().getProperties(); + assertEquals(foundProperties.get(financeLocationFieldName), "LondonBridge", + "finance:Location property is expected to be defined with 'LondonBridge' as value"); + assertNull(foundProperties.get(parkingLocationFieldName), "finance:ParkingLocation should not be included " + + "into the document anymore"); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchCasesTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchCasesTest.java new file mode 100644 index 0000000000..de1fd214f2 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchCasesTest.java @@ -0,0 +1,251 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import java.util.ArrayList; +import java.util.List; + +import org.hamcrest.Matchers; +import org.springframework.http.HttpStatus; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.utility.Utility; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; + +public class SearchCasesTest extends AbstractSearchServicesE2ETest +{ + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + searchServicesDataPreparation(); + Assert.assertTrue(waitForContentIndexing(file4.getContent(), true)); + } + + @Test(priority = 1) + public void testSearchNameField() + { + SearchResponse response = queryAsUser(testUser, "cm:name:pangram"); + restClient.assertStatusCodeIs(HttpStatus.OK); + response.assertThat().entriesListIsNotEmpty(); + } + + @Test(priority = 2) + public void testSearchTitleField() + { + SearchResponse response2 = queryAsUser(testUser, "cm:title:cars"); + restClient.assertStatusCodeIs(HttpStatus.OK); + response2.assertThat().entriesListIsNotEmpty(); + } + + @Test(priority = 3) + public void testSearchDescriptionField() + { + SearchResponse response3 = queryAsUser(testUser, "cm:description:alfresco"); + restClient.assertStatusCodeIs(HttpStatus.OK); + response3.assertThat().entriesListIsNotEmpty(); + } + + @Test(priority = 4) + public void testSearchTextFile() + { + SearchResponse response6 = queryAsUser(testUser, "cm:name:pangram.txt"); + restClient.assertStatusCodeIs(HttpStatus.OK); + response6.assertThat().entriesListIsNotEmpty(); + } + + @Test(priority = 5) + public void testSearchPDFFile() + { + SearchResponse response6 = queryAsUser(testUser, "cm:name:cars.PDF"); + restClient.assertStatusCodeIs(HttpStatus.OK); + response6.assertThat().entriesListIsNotEmpty(); + } + + @Test(priority = 6) + public void testSearchODTFile() + { + SearchResponse response6 = queryAsUser(testUser, "cm:name:" + file4.getName()); + restClient.assertStatusCodeIs(HttpStatus.OK); + response6.assertThat().entriesListIsNotEmpty(); + } + + @Test(priority = 7) + public void testSearchPhraseQueries() + { + SearchResponse response6 = queryAsUser(testUser, "The quick brown fox jumps over the lazy dog"); + restClient.assertStatusCodeIs(HttpStatus.OK); + response6.assertThat().entriesListIsNotEmpty(); + } + + @Test(priority = 8) + public void testSearchExactTermQueries() + { + SearchResponse response6 = queryAsUser(testUser, "=alfresco"); + restClient.assertStatusCodeIs(HttpStatus.OK); + response6.assertThat().entriesListIsNotEmpty(); + } + + @Test(priority = 9) + public void testSearchConjunctionQueries() + { + SearchResponse response6 = queryAsUser(testUser, "unique AND search"); + restClient.assertStatusCodeIs(HttpStatus.OK); + response6.assertThat().entriesListIsNotEmpty(); + } + + @Test(priority = 10) + public void testSearchDisjunctionQueries() + { + SearchResponse response6 = queryAsUser(testUser, "file OR discovery"); + restClient.assertStatusCodeIs(HttpStatus.OK); + response6.assertThat().entriesListIsNotEmpty(); + } + + @Test(priority = 11) + public void testSearchNegationQueries() + { + SearchResponse response6 = queryAsUser(testUser, "pangram NOT pan"); + restClient.assertStatusCodeIs(HttpStatus.OK); + response6.assertThat().entriesListIsNotEmpty(); + } + + @Test(priority = 12) + public void testSearchWildcardQueries() + { + SearchResponse response6 = queryAsUser(testUser, "al?res*"); + restClient.assertStatusCodeIs(HttpStatus.OK); + response6.assertThat().entriesListIsNotEmpty(); + } + + @Test(priority = 13) + public void testSearchUpdateContent() throws InterruptedException + { + String originalText = String.valueOf(System.currentTimeMillis()); + String newText = String.valueOf(System.currentTimeMillis() + 300000); + + // Create test file to be accessed only by this test method to avoid inconsistent results when querying updates + FileModel updateableFile = createFileWithProvidedText(originalText + ".txt", originalText); + + // Verify that 1 occurrence of the original text is found + SearchResponse response1 = queryAsUser(testUser, "cm:content:" + originalText); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response1.getEntries().size(), 1, "Expected 1 original text before update"); + + // Verify that 0 occurrences of the replacement text are found + SearchResponse response2 = queryAsUser(testUser, "cm:content:" + newText); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response2.getEntries().size(), 0, "Expected 0 new text before update"); + + // Update the content + String newContent = "Description: Contains provided string: " + newText; + dataContent.usingUser(adminUserModel).usingSite(testSite).usingResource(updateableFile) + .updateContent(newContent); + Assert.assertTrue(waitForContentIndexing(newText, true)); + + // Verify that 0 occurrences of the original text are found + SearchResponse response3 = queryAsUser(testUser, "cm:content:" + originalText); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response3.getEntries().size(), 0, "Expected 0 original text after update"); + + // Verify that 1 occurrence of the replacement text is found + SearchResponse response4 = queryAsUser(testUser, "cm:content:" + newText); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response4.getEntries().size(), 1, "Expected 1 new text before update"); + } + + /** + * { "query": { "query": "*" }, "facetFields": { "facets": [{"field": "cm:mimetype"},{"field": "modifier"}] } } + */ + @Test(priority = 14) + public void searchWithFacedFields() throws InterruptedException + { + String uniqueText = String.valueOf(System.currentTimeMillis()); + + // Create test file to be accessed only by this test method to avoid inconsistent results + createFileWithProvidedText(uniqueText + ".ODT", uniqueText); + + SearchRequest query = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("cm:content:" + uniqueText); + query.setQuery(queryReq); + + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List facets = new ArrayList<>(); + facets.add(new RestRequestFacetFieldModel("cm:content.mimetype")); + facets.add(new RestRequestFacetFieldModel("modifier")); + facetFields.setFacets(facets); + query.setFacetFields(facetFields); + + SearchResponse response = query(query); + + Assert.assertNotNull(response.getContext().getFacetsFields()); + Assert.assertFalse(response.getContext().getFacetsFields().isEmpty()); + Assert.assertNull(response.getContext().getFacetQueries()); + Assert.assertNull(response.getContext().getFacets()); + + // Look up the "modifier" facet by label instead of relying on response ordering + RestResultBucketsModel modifierFacet = response.getContext().getFacetsFields().stream() + .filter(f -> "modifier".equals(f.getLabel())) + .findFirst() + .orElseThrow(() -> new AssertionError("'modifier' facet not found in response")); + + modifierFacet.assertThat().field("label").is("modifier"); + FacetFieldBucket bucket1 = modifierFacet.getBuckets().getFirst(); + bucket1.assertThat().field("label").is(testUser.getUsername()); + bucket1.assertThat().field("display").is("FN-" + testUser.getUsername() + " LN-" + testUser.getUsername()); + bucket1.assertThat().field("filterQuery").is("modifier:\"" + testUser.getUsername() + "\""); + bucket1.assertThat().field("count").is(1); + } + + @Test(priority = 15) + public void searchSpecialCharacters() + { + String specialCharfileName = "è¥äæ§ç§-åæ.pdf"; + FileModel file = new FileModel(specialCharfileName, "è¥äæ§ç§-忬¯¸" + "è¥äæ§ç§-忬¯¸", "è¥äæ§ç§-忬¯¸", FileType.TEXT_PLAIN, + "Text file with Special Characters: " + specialCharfileName); + dataContent.usingUser(testUser).usingSite(testSite).createContent(file); + + waitForIndexing(file.getName(), true); + + SearchRequest searchReq = createQuery("name:'" + specialCharfileName + "'"); + SearchResponse nodes = query(searchReq); + restClient.assertStatusCodeIs(HttpStatus.OK); + + int searchCount = 0; + while (nodes.isEmpty() && searchCount < SEARCH_MAX_ATTEMPTS) + { + // Wait for the solr indexing (eventual consistency). + Utility.waitToLoopTime(properties.getSolrWaitTimeInSeconds(), "Wait For Results After Indexing. Retry Attempt: " + (searchCount + 1)); + nodes = query(searchReq); + restClient.assertStatusCodeIs(HttpStatus.OK); + searchCount++; + } + + nodes.assertThat().entriesListIsNotEmpty(); + restClient.onResponse().assertThat().body("list.entries.entry[0].name", Matchers.equalToIgnoringCase(specialCharfileName)); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchExactTermTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchExactTermTest.java new file mode 100644 index 0000000000..b2f8dcb4f5 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchExactTermTest.java @@ -0,0 +1,248 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import org.testng.annotations.Test; + +/** + * Tests Exact Term queries against standard properties (cm:name, cm:title, cm:description, cm:content). These tests are engine-agnostic and do not require Cross Locale or Solr-specific tokenisation configuration. + */ +@SuppressWarnings("PMD.MethodNamingConventions") +public class SearchExactTermTest extends AbstractSearchExactTermTest +{ + /** + * Note these tests are searching in cm:name property only (default for exact term without cross locale). 1 result expected for =run >> Document #2 >> name: "Run" 1 result expected for =jump >> Document #4 >> name: "Jump" + */ + @Test + public void exactSearch_singleTerm_shouldReturnResultsContainingExactTermInName() + { + /* 1 result is expected: - Document #2 >> name: "Run" */ + assertResponseCardinality("=run", 1); + + /* No result for runner in cm:name property, one record has runners in "description" property. You can see the difference between exact search and not */ + assertResponseCardinality("=runner", 0); + assertResponseCardinality("runner", 1); + + /* 1 result is expected: - Document #4 >> name: "Jump" */ + assertResponseCardinality("=jump", 1); + } + + @Test + public void exactSearch_singleTermConjunction_shouldReturnFullFieldValueMatch() throws Exception + { + /** + * Since REST API is getting the results from DB or Search Services, using single term expressions is always retrieved from DB when using default configuration "solr.query.fts.queryConsistency=TRANSACTIONAL_IF_POSSIBLE". Combining this single term with range queries (like cm:created) will ensure the results are coming from SOLR in this mode. + */ + + /* 1 result is expected for non-tokenised field (tok:false) - Document #4 >> title: "Running" */ + assertResponseCardinality("=tok:false:Running AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 1); + + /* 0 results are expected: there is no result that have exactly cm:title:"Run" The closest we have is record Run (tok:false:"Run : a philosophy") As you can see we don't have a full match, so it's not in the results. */ + assertResponseCardinality("=tok:false:Run AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 0); + } + + /** + * These tests should be re-enabled once the following tickets have been solved: - https://alfresco.atlassian.net/browse/SEARCH-2461 - https://alfresco.atlassian.net/browse/SEARCH-2953 + */ + @Test(enabled = false) + public void failing_exactSearch_singleTermConjunction_shouldReturnFullFieldValueMatch() throws Exception + { + // SEARCH-2953 + assertResponseCardinality("=tok:false:running AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 1); + + // SEARCH-2461 + assertResponseCardinality("=tok:false:running", 1); + + // SEARCH-2461 + assertResponseCardinality("=tok:false:Running", 1); + + // SEARCH-2461 + assertResponseCardinality("=tok:false:Run", 0); + } + + @Test + public void exactSearch_singleTermConjunction_shouldReturnException() throws Exception + { + /** + * Since REST API is getting the results from DB or Search Services, using single term expressions is always retrieved from DB when using default configuration "solr.query.fts.queryConsistency=TRANSACTIONAL_IF_POSSIBLE". Combining this single term with range queries (like cm:created) will ensure the results are coming from SOLR in this mode. + */ + + /** + * Unsupported Exception is expected when using exact term search with tokenised properties + */ + assertException("=tok:true:running AND cm:created:['" + fromDate + "' TO '" + toDate + "']"); + assertException("=tok:both:running AND cm:created:['" + fromDate + "' TO '" + toDate + "']"); + + /** + * Unsupported Exception is expected when using exact term search with tokenised properties + */ + assertException("=tok:true:Running AND cm:created:['" + fromDate + "' TO '" + toDate + "']"); + assertException("=tok:both:Running AND cm:created:['" + fromDate + "' TO '" + toDate + "']"); + + /** + * Unsupported Exception is expected when using exact term search with tokenised properties + */ + assertException("=tok:true:Run AND cm:created:['" + fromDate + "' TO '" + toDate + "']"); + assertException("=tok:both:Run AND cm:created:['" + fromDate + "' TO '" + toDate + "']"); + } + + /** + * These tests should be re-enabled once the following tickets have been solved: - https://alfresco.atlassian.net/browse/SEARCH-2461 + */ + @Test(enabled = false) + public void failing_exactSearch_singleTermConjunction_shouldReturnException() throws Exception + { + // SEARCH-2461 + assertException("=tok:true:running"); + assertException("=tok:both:running"); + + // SEARCH-2461 + assertException("=tok:true:Running"); + assertException("=tok:both:Running"); + + // SEARCH-2461 + assertException("=tok:true:Run"); + assertException("=tok:both:Run"); + } + + @Test + public void exactSearch_multiTerm_shouldReturnResultsContainingExactTerm() + { + /* 2 results are expected: - Document #2 >> name: "Run" - Document #4 >> name: "Jump" */ + assertResponseCardinality("=run =jump", 2); + + /* No result for runner or jumper in cm:name property One document has runners and another record has jumpers in description You can see the difference between exact search and not */ + assertResponseCardinality("=runner =jumper", 0); + assertResponseCardinality("runner jumper", 2); + + /* 2 results are expected: - Document #1 >> name: "Running" - Document #5 >> name: "Running jumping" */ + assertResponseCardinality("=running =jumping", 2); + } + + @Test + public void exactSearch_multiTermInFieldWithOnlyUnTokenizedAnalysis_shouldReturnFullFieldValueMatch() throws Exception + { + /* 1 result is expected - Document #4 >> title: "Running" */ + assertResponseCardinality("=tok:false:Running =tok:false:jumpers AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 1); + + /** + * Unsupported Exception is expected when using exact term search with tokenised properties + */ + assertException("=tok:both:running =tok:both:jumpers AND cm:created:['" + fromDate + "' TO '" + toDate + "']"); + assertException("=tok:true:running =tok:true:jumpers AND cm:created:['" + fromDate + "' TO '" + toDate + "']"); + } + + /** + * These tests should be re-enabled once the following tickets have been solved: - https://alfresco.atlassian.net/browse/SEARCH-2461 - https://alfresco.atlassian.net/browse/SEARCH-2953 + */ + @Test(enabled = false) + public void failing_exactSearch_multiTermInFieldWithOnlyUnTokenizedAnalysis_shouldReturnFullFieldValueMatch() throws Exception + { + // SEARCH-2953 + assertResponseCardinality("=tok:false:running =tok:false:jumpers AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 1); + + // SEARCH-2461 + assertResponseCardinality("=tok:false:running =tok:false:jumpers", 1); + assertException("=tok:both:running =tok:both:jumpers"); + assertException("=tok:true:running =tok:true:jumpers"); + } + + @Test + public void exactSearch_exactPhrase_shouldReturnResultsContainingExactPhrase() + { + /* No result for "run jump" in cm:name property */ + assertResponseCardinality("=\"run jump\"", 0); + + /* No result for "runner jumper" in cm:name property One document has runners jumpers in description You can see the difference between exact search and not */ + assertResponseCardinality("=\"runner jumper\"", 0); + assertResponseCardinality("\"runner jumper\"", 1); + + /* 1 result is expected for exact term search: - Document #5 >> name: "Running jumping" */ + assertResponseCardinality("=\"running jumping\"", 1); + + /* 5 results are expected for non-exact phrase search: - Document #1 >> name: "Running", title: "Running jumping" - Document #2 >> name: "Run", description: "you are supposed to run jump" - Document #3 >> title: "Running jumping twice jumpers" - Document #4 >> content: "runnings jumpings", title: "Running" - Document #5 >> name: "Running jumping", title: "Running the art of jumping" */ + assertResponseCardinality("\"running jumping\"", 5); + } + + @Test + public void exactSearch_phraseInFieldConjunction_shouldReturnFullFieldValueMatch() throws Exception + { + /* 1 result is expected for exact term search - Document #5 >> name: "Running jumping" */ + assertResponseCardinality("=tok:false:\"Running jumping\" AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 1); + + /* No result for "Running jumping twice" in cm:name property is expected */ + assertResponseCardinality("=tok:false:\"Running jumping twice\" AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 0); + } + + /** + * These tests should be re-enabled once the following tickets have been solved: - https://alfresco.atlassian.net/browse/SEARCH-2461 - https://alfresco.atlassian.net/browse/SEARCH-2953 + */ + @Test(enabled = false) + public void failing_exactSearch_phraseInFieldConjunction_shouldReturnFullFieldValueMatch() throws Exception + { + // SEARCH-2953 + assertResponseCardinality("=tok:false:\"running jumping\" AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 1); + + // SEARCH-2461 + assertResponseCardinality("=tok:false:\"running jumping\"", 1); + assertResponseCardinality("=tok:false:\"Running jumping\"", 1); + + // SEARCH-2461 + assertResponseCardinality("=tok:false:\"Running jumping twice\"", 0); + } + + @Test + public void exactSearch_phraseInFieldConjunction_shouldReturnOrException() throws Exception + { + /** + * Unsupported Exception is expected when using exact term search with tokenised properties + */ + assertException("=tok:true:\"running jumping\" AND cm:created:['" + fromDate + "' TO '" + toDate + "']"); + assertException("=tok:both:\"running jumping\" AND cm:created:['" + fromDate + "' TO '" + toDate + "']"); + + /** + * Unsupported Exception is expected when using exact term search with tokenised properties + */ + assertException("=tok:true:\"Running jumping twice\" AND cm:created:['" + fromDate + "' TO '" + toDate + "']"); + assertException("=tok:both:\"Running jumping twice\" AND cm:created:['" + fromDate + "' TO '" + toDate + "']"); + } + + /** + * These tests should be re-enabled once the following tickets have been solved: - https://alfresco.atlassian.net/browse/SEARCH-2461 + */ + @Test(enabled = false) + public void failing_exactSearch_phraseInFieldConjunction_shouldReturnException() throws Exception + { + // SEARCH-2461 + assertException("=tok:true:\"Running jumping\""); + assertException("=tok:both:\"Running jumping\""); + + // SEARCH-2461 + assertException("=tok:true:\"Running jumping twice\""); + assertException("=tok:both:\"Running jumping twice\""); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchHighLightTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchHighLightTest.java new file mode 100644 index 0000000000..c9d68c2f6d --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchHighLightTest.java @@ -0,0 +1,128 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.report.Bug; + +/** + * Search high lighting test. + */ +@SuppressWarnings("PMD.UselessParentheses") +public class SearchHighLightTest extends AbstractSearchServicesE2ETest +{ + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + searchServicesDataPreparation(); + FileModel fileHl = new FileModel("very long name", "some title", "description", FileType.TEXT_PLAIN, + "Content of long name "); + dataContent.usingUser(testUser).usingSite(testSite).usingResource(folder).createContent(fileHl); + waitForMetadataIndexing(fileHl.getName(), true); + } + + @Test + @Bug(id = "TAS-3220") + public void searchWithHighLight() throws Exception + { + waitForContentIndexing(file2.getContent(), true); + + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("cm:content:cars"); + queryReq.setUserQuery("cars"); + + RestRequestHighlightModel highlight = new RestRequestHighlightModel(); + highlight.setPrefix("¿"); + highlight.setPostfix("?"); + highlight.setMergeContiguous(true); + List fields = new ArrayList<>(); + fields.add(RestRequestFieldsModel.of("cm:content")); + highlight.setFields(fields); + SearchResponse nodes = query(queryReq, highlight); + nodes.assertThat().entriesListIsNotEmpty(); + ResponseHighlightModel hl = nodes.getEntryByIndex(0).getSearch().getHighlight().getFirst(); + assertTrue(hl.getSnippets().contains("The landrover discovery is not a sports ¿car?")); + } + + @Test + public void searchNonIndexedData() + { + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("cm:title"); + queryReq.setUserQuery("zoro"); + + RestRequestHighlightModel highlight = new RestRequestHighlightModel(); + highlight.setPrefix("¿"); + highlight.setPostfix("?"); + highlight.setMergeContiguous(true); + List fields = new ArrayList<>(); + fields.add(RestRequestFieldsModel.of("cm:title")); + highlight.setFields(fields); + SearchResponse nodes = query(queryReq, highlight); + nodes.assertThat().entriesListDoesNotContain("highlight"); + } + + @Test + public void searchDataConjunctionGenericQuery() + { + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + + queryReq.setQuery("very AND name"); + + RestRequestHighlightModel highlight = new RestRequestHighlightModel(); + highlight.setPrefix("¿"); + highlight.setPostfix("?"); + highlight.setMergeContiguous(false); + List fields = new ArrayList<>(); + fields.add(RestRequestFieldsModel.of("cm:name")); + highlight.setFields(fields); + SearchResponse nodes = query(queryReq, highlight); + nodes.assertThat().entriesListDoesNotContain("highlight"); + + String expectedHighlight = "¿very? long ¿name?"; + + assertEquals(1, nodes.getEntries().size()); + + nodes.getEntries().stream() + .map(SearchNodeModel::getModel) + .map(SearchNodeModel::getSearch) + .map(SearchScoreModel::getHighlight).forEach((hl -> { + assertEquals(1, hl.size()); + assertEquals(expectedHighlight, hl.getFirst().getSnippets().getFirst()); + })); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchNonIndexedFields.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchNonIndexedFields.java new file mode 100644 index 0000000000..1ca9bde029 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchNonIndexedFields.java @@ -0,0 +1,128 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import java.util.HashMap; +import java.util.Map; + +import io.restassured.RestAssured; +import org.apache.chemistry.opencmis.commons.PropertyIds; +import org.apache.chemistry.opencmis.commons.enums.VersioningState; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.core.RestRequest; +import org.alfresco.rest.model.RestTextResponse; +import org.alfresco.utility.constants.UserRole; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.FolderModel; + +/** + * Testing that fields declared as non indexed (index="true" in Alfresco Content Model) are not created in SOLR Schema and that are not stored in SOLR Index. + * + */ +public class SearchNonIndexedFields extends AbstractSearchServicesE2ETest +{ + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + serverHealth.assertServerIsOnline(); + + deployCustomModel("indexing-disabled-content-model.xml"); + + dataUser.addUserToSite(testUser, testSite, UserRole.SiteContributor); + + FolderModel testFolder = dataContent.usingSite(testSite).usingUser(testUser).createFolder(); + + FileModel sampleFile = FileModel.getRandomFileModel(FileType.TEXT_PLAIN, "Sample"); + sampleFile.setName("in1-" + sampleFile.getName()); + + // One field indexed and one field not indexed + Map properties = new HashMap<>(); + properties.put(PropertyIds.OBJECT_TYPE_ID, "D:index:sample"); + properties.put(PropertyIds.NAME, sampleFile.getName()); + properties.put("index:indexed", "Indexed"); + properties.put("index:nonIndexed", "Not indexed"); + + cmisApi.authenticateUser(testUser).usingSite(testSite).usingResource(testFolder).createFile(sampleFile, properties, VersioningState.MAJOR) + .assertThat().existsInRepo(); + + sampleFile = FileModel.getRandomFileModel(FileType.TEXT_PLAIN, "Sample"); + sampleFile.setName("in2-" + sampleFile.getName()); + + // Only one field not indexed + properties = new HashMap<>(); + properties.put(PropertyIds.OBJECT_TYPE_ID, "D:index:sample"); + properties.put(PropertyIds.NAME, sampleFile.getName()); + properties.put("index:nonIndexed", "Not indexed"); + + cmisApi.authenticateUser(testUser).usingSite(testSite).usingResource(testFolder).createFile(sampleFile, properties, VersioningState.MAJOR) + .assertThat().existsInRepo(); + + waitForIndexing(sampleFile.getName(), true); + } + + /** + * Test that non indexed fields are not stored in SOLR Index + */ + @Test(priority = 1) + public void testIndexedAndNonIndexedField() + { + SearchResponse response = queryAsUser(testUser, "index_indexed:[* TO *]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "index_nonIndexed:[* TO *]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 0); + } + + /** + * Test that non indexed fields are not created in SOLR Schema + */ + @Test(priority = 2) + public void testNonIndexedDoesNotExist() + { + RestAssured.basePath = "solr/alfresco"; + + restClient.configureSolrEndPoint(); + restClient.configureRequestSpec().setBasePath(RestAssured.basePath); + + RestRequest request = RestRequest.simpleRequest(HttpMethod.GET, "admin/luke"); + RestTextResponse response = restClient.processTextResponse(request); + String bodyResponse = response.getResponse().getBody().print(); + + Assert.assertTrue(bodyResponse.contains("{http://www.alfresco.org/model/index/1.0}indexed"), + "Expecting index:indexed field to be present in SOLR Schema"); + Assert.assertFalse(bodyResponse.contains("{http://www.alfresco.org/model/index/1.0}nonIndexed"), + "Expecting index:nonIndexed field NOT to be present in SOLR Schema"); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchParentFieldTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchParentFieldTest.java new file mode 100644 index 0000000000..6f7e47ce7d --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchParentFieldTest.java @@ -0,0 +1,87 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.utility.data.DataGroup; +import org.alfresco.utility.model.GroupModel; + +/** + * Test class tests PARENT field is including all the PARENT Nodes Created for SEARCH-2378 + */ +public class SearchParentFieldTest extends AbstractSearchServicesE2ETest +{ + @Autowired + protected DataGroup dataGroup; + + List groups; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + groups = new ArrayList<>(); + groups.add(dataGroup.createRandomGroup()); + groups.add(dataGroup.createRandomGroup()); + + dataGroup.addListOfUsersToGroup(groups.get(0), testUser); + dataGroup.addListOfUsersToGroup(groups.get(1), testUser); + + waitForIndexing( + "TYPE:'cm:authorityContainer' AND cm:authorityName:'GROUP_" + groups.get(1).getGroupIdentifier() + "'", + true); + } + + /** + * Test users in groups can be found using PARENT expressions. + */ + @Test(priority = 1) + public void testSearchParentForPerson() + { + for (GroupModel group : groups) + { + // Find groupId to be used in the PARENT expression + String queryGroup = "TYPE:'cm:authorityContainer' AND cm:authorityName:'GROUP_" + group.getGroupIdentifier() + + "'"; + SearchResponse response = queryAsUser(dataUser.getAdminUser(), queryGroup); + String groupId = response.getEntries().getFirst().getModel().getId(); + + // Find the user assigned as member of this group with PARENT clause + String queryParentGroup = "(TYPE:'cm:person' OR TYPE:'cm:authorityContainer') AND PARENT:'workspace://SpacesStore/" + + groupId + "'"; + + response = queryAsUser(dataUser.getAdminUser(), queryParentGroup); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1, "Expecting 1 user (" + testUser.getUsername() + + ") as member of this group (" + group.getGroupIdentifier() + ")"); + } + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchPermissionsTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchPermissionsTest.java new file mode 100644 index 0000000000..2e2265acea --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchPermissionsTest.java @@ -0,0 +1,209 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import jakarta.json.Json; +import jakarta.json.JsonObject; + +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.dataprep.SiteService.Visibility; +import org.alfresco.utility.constants.UserRole; +import org.alfresco.utility.data.RandomData; +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.UserModel; + +/** + * Search end point Public API test with Permission checks + */ +@SuppressWarnings({"PMD.SingularField", "PMD.OneDeclarationPerLine"}) +public class SearchPermissionsTest extends AbstractSearchServicesE2ETest +{ + private FileModel file1, file2; + private FolderModel parentFolder, folder1, folder2; + private UserModel testUser1, testUser2, testUser3; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + /* Create the following file structure in the same Site : In addition to the preconditions created in dataPreparation |- permGrandParent |-- permChild1 |------ permFile1 |-- permChild2 |------ permFile2 |-- permChild3 (Later: In test 2) */ + + parentFolder = new FolderModel("permGrandParent"); + dataContent.usingUser(testUser).usingSite(testSite).createFolder(parentFolder); + folder1 = dataContent.usingUser(testUser).usingSite(testSite).usingResource(parentFolder).createFolderCmisApi("permChild1"); + folder2 = dataContent.usingUser(testUser).usingSite(testSite).usingResource(parentFolder).createFolderCmisApi("permChild2"); + + file1 = new FileModel("permFile1", FileType.TEXT_PLAIN, "File1 with inherited permissions"); + file2 = new FileModel("permFile2", FileType.TEXT_PLAIN, "File2 with inherited permissions"); + + // Create test users + testUser1 = dataUser.createRandomTestUser("UserSiteMember"); + testUser2 = dataUser.createRandomTestUser("UserNotASiteMemeber"); + testUser3 = dataUser.createRandomTestUser("UserWithoutContentAccess"); + + dataUser.addUserToSite(testUser1, testSite, UserRole.SiteCollaborator); + + dataContent.usingUser(testUser).usingSite(testSite).usingResource(folder1).createContent(file1); + dataContent.usingUser(testUser).usingSite(testSite).usingResource(folder2).createContent(file2); + + // Deny testUser1 - access to file1 + JsonObject userPermission = Json + .createObjectBuilder() + .add("permissions", + Json.createObjectBuilder() + .add("isInheritanceEnabled", false) + .add("locallySet", + Json.createObjectBuilder() + .add("authorityId", testUser1.getUsername()) + .add("name", "SiteCollaborator") + .add("accessStatus", "DENIED"))) + .build(); + String putBody = userPermission.toString(); + restClient.authenticateUser(testUser).withCoreAPI().usingNode(file1).updateNode(putBody); + + // Allow testUser2 - access to file2, user is not a site member + userPermission = Json + .createObjectBuilder() + .add("permissions", + Json.createObjectBuilder() + .add("isInheritanceEnabled", false) + .add("locallySet", + Json.createObjectBuilder() + .add("authorityId", testUser2.getUsername()) + .add("name", "SiteContributor") + .add("accessStatus", "ALLOWED"))) + .build(); + putBody = userPermission.toString(); + restClient.authenticateUser(testUser).withCoreAPI().usingNode(file2).updateNode(putBody); + + waitForContentIndexing(file2.getContent(), true); + } + + @Test(priority = 1) + public void searchResultsRespectInheritedPermissions() + { + /** + * Private Site Folder Structure available for this test |- permGrandParent |-- permChild1 |------ permFile1 (inheritance disabled, deny permission to testUser1) |-- permChild2 |------ permFile2 (inheritance disabled, allow permission to testUser2) + */ + // Search as testUser: expect all: 5 results: When user is a Site Manager + SearchResponse response = queryAsUser(testUser, "cm:name:perm*"); + int resultCount = response.getPagination().getCount(); + Assert.assertEquals(resultCount, 5, "Unexpected Result count for testUser: Expected 5, received: " + resultCount); + + // Search as testUser1: expect 3 results: when user is a site member but without permission to a content + response = queryAsUser(testUser1, "cm:name:perm*"); + resultCount = response.getPagination().getCount(); + Assert.assertEquals(resultCount, 3, "Unexpected Result count for testUser1: Expected 3, received: " + resultCount); + + // Search as testUser2: expect 1 result: When user isn't a site member but has granular permissions to a content + response = queryAsUser(testUser2, "cm:name:perm*"); + resultCount = response.getPagination().getCount(); + Assert.assertEquals(resultCount, 1, "Unexpected Result count for testUser2: Expected 1, received: " + resultCount); + + // Search as testUser3: expect none: 0 results: When user isn't a site member / does not have granular permissions to the content + response = queryAsUser(testUser3, "cm:name:perm*"); + resultCount = response.getPagination().getCount(); + Assert.assertEquals(resultCount, 0, "Unexpected Result count for testUser3: Expected 0, received: " + resultCount); + } + + @Test(priority = 2) + public void searchResultsRespectInheritedPermissionsDisabled() + { + // Create folder + FolderModel folder3 = dataContent.usingUser(testUser).usingSite(testSite).usingResource(parentFolder).createFolderCmisApi("permChild3"); + + // Turn off inherited permissions for folder3 + JsonObject userPermission = Json.createObjectBuilder().add("permissions", Json.createObjectBuilder().add("isInheritanceEnabled", false)).build(); + String putBody = userPermission.toString(); + restClient.authenticateUser(testUser).withCoreAPI().usingNode(folder3).updateNode(putBody); + + // Wait for indexing + waitForIndexing(folder3.getName(), true); + + /** + * Private Site Folder Structure available now for this test |- permGrandParent |-- permChild1 |------ permFile1 (inheritance disabled, deny permission to testUser1) |-- permChild2 |------ permFile2 (inheritance disabled, allow permission to testUser2) |-- permChild3 (inheritance disabled) + */ + + // Search as testUser: expect all: 6 results: When user is a Site Manager + SearchResponse response = queryAsUser(testUser, "cm:name:perm*"); + int resultCount = response.getPagination().getCount(); + Assert.assertEquals(resultCount, 6, "Unexpected Result count for testUser: Expected 6, received: " + resultCount); + + // Search as testUser1: expect 3 results: when user is a site member but without permission to a content + response = queryAsUser(testUser1, "cm:name:perm*"); + resultCount = response.getPagination().getCount(); + Assert.assertEquals(resultCount, 3, "Unexpected Result count for testUser1: Expected 3, received: " + resultCount); + + // Search as testUser2: expect 1 result: When user isn't a site member but has granular permissions to a content + response = queryAsUser(testUser2, "cm:name:perm*"); + resultCount = response.getPagination().getCount(); + Assert.assertEquals(resultCount, 1, "Unexpected Result count for testUser2: Expected 1, received: " + resultCount); + + // Search as testUser3: expect none: 0 results: When user isn't a site member / does not have granular permissions to the content + response = queryAsUser(testUser3, "cm:name:perm*"); + resultCount = response.getPagination().getCount(); + Assert.assertEquals(resultCount, 0, "Unexpected Result count for testUser3: Expected 0, received: " + resultCount); + } + + @Test(priority = 3) + public void searchResultsOnChangingSiteVisibility() + { + // Create Site + SiteModel testPermissionsSite = new SiteModel(RandomData.getRandomName("SiteSearchPermissions")); + testPermissionsSite.setVisibility(Visibility.PUBLIC); + + dataSite.usingUser(adminUserModel).createSite(testPermissionsSite); + + // Add Users to the site + dataUser.addUserToSite(testUser, testPermissionsSite, UserRole.SiteCollaborator); + + // Create a folder + String folderName = "Folder" + unique_searchString; + FolderModel folder = dataContent.usingUser(testUser).usingSite(testPermissionsSite).createFolderCmisApi(folderName); + + // Query + Assert.assertTrue(waitForIndexing(folder.getName(), true), "Folder isn't yet indexed"); + + // Edit Site Visibility + testPermissionsSite.setDescription("PrivateSite".concat(testPermissionsSite.getDescription())); + testPermissionsSite.setVisibility(Visibility.PRIVATE); + restClient.authenticateUser(adminUserModel).withCoreAPI().usingSite(testPermissionsSite).updateSite(testPermissionsSite); + + // Wait for indexing + Assert.assertTrue(waitForIndexing("description:" + testPermissionsSite.getDescription(), true), "New Site Description isn't yet been indexed"); + + // Query + SearchResponse response = queryAsUser(testUser, folderName); + int resultCount = response.getPagination().getCount(); + Assert.assertEquals(resultCount, 1, "Unexpected Result count for testUser: Expected 1, received: " + resultCount); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchQueryPaginationTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchQueryPaginationTest.java new file mode 100644 index 0000000000..bd0171efff --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchQueryPaginationTest.java @@ -0,0 +1,276 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.UserModel; + +/** + * The purpose of this test is to test search query pagination using cmis and afts query + */ +@SuppressWarnings({"PMD.SingularField", "PMD.UselessParentheses"}) +public class SearchQueryPaginationTest extends AbstractSearchServicesE2ETest +{ + private UserModel testUser2; + private FolderModel testFolder; + private FileModel testFile; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + // Create testUser2: This user does not have access to the testSite + testUser2 = dataUser.createRandomTestUser("testUser2"); + + // Create a new folder and 10 files inside the folder + testFolder = dataContent.usingUser(testUser).usingSite(testSite).createFolder(); + + for (int i = 0; i < 10; i++) + { + testFile = new FileModel(i + "-File.txt", unique_searchString, "", FileType.TEXT_PLAIN); + dataContent.usingUser(testUser).usingSite(testSite).usingResource(testFolder).createContent(testFile); + } + + // Wait for every file individually + for (int i = 0; i < 10; i++) + { + waitForMetadataIndexing(i + "-File.txt", true); + } + + // wait until the CMIS IN_FOLDER query itself returns all files as AFTS name-based indexing and CMIS indexing can have different lag. + String parentId = testFolder.getNodeRefWithoutVersion(); + String cmisQuery = "select * from cmis:document where IN_FOLDER('" + parentId + "')"; + waitForCmisSearchCount(testUser, cmisQuery, 10); + } + + @Test(priority = 1) + public void testCmisSearchWithPagination() + { + // Scope to testFolder only to get a stable, predictable count + String parentId = testFolder.getNodeRefWithoutVersion(); + String query = "select * from cmis:document where IN_FOLDER('" + parentId + "')"; + + // Set skipCount = 0, maxItems = 1000: all results fit in the page, exact total returned + SearchResponse response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(0, 1000)); + Integer totalDocuments = response.getPagination().getTotalItems(); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = totalDocuments, maxItems = 1000: nothing remains, exact total returned + response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(totalDocuments, 1000)); + Assert.assertEquals(response.getPagination().getTotalItems(), totalDocuments, "Total Document Count doesn't match"); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = 0, maxItems = totalDocuments/2: page is smaller than remaining results. + // Elasticsearch returns an approximate totalItems (= maxItems+1) rather than the exact + // total when hasMoreItems=true. Only hasMoreItems is asserted here. + response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(0, totalDocuments / 2)); + Assert.assertTrue(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = 1, maxItems = totalDocuments-2: page is still smaller than remaining. + // Same Elasticsearch approximate-total caveat applies. + response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(1, totalDocuments - 2)); + Assert.assertEquals(response.getPagination().getTotalItems(), totalDocuments, "Total Document Count doesn't match"); + Assert.assertTrue(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = totalDocuments, maxItems = totalDocuments/2: nothing remains, exact total + response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(totalDocuments, totalDocuments / 2)); + Assert.assertEquals(response.getPagination().getTotalItems(), totalDocuments, "Total Document Count doesn't match"); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = 0, maxItems = totalDocuments: all results fit exactly, exact total + response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(0, totalDocuments)); + Assert.assertEquals(response.getPagination().getTotalItems(), totalDocuments, "Total Document Count doesn't match"); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = totalDocuments/2+1, maxItems = totalDocuments: remaining fits, exact total + response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(totalDocuments / 2 + 1, totalDocuments)); + Assert.assertEquals(response.getPagination().getTotalItems(), totalDocuments, "Total Document Count doesn't match"); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = totalDocuments, maxItems = totalDocuments: nothing remains, exact total + response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(totalDocuments, totalDocuments)); + Assert.assertEquals(response.getPagination().getTotalItems(), totalDocuments, "Total Document Count doesn't match"); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = 0, maxItems = 0 + response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(0, 0)); + Assert.assertTrue(response.isEmpty(), "Empty Response, Error is expected when maxItems is <= 0"); + + // Set skipCount = totalDocuments, maxItems = 0 + response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(totalDocuments, 0)); + Assert.assertTrue(response.isEmpty(), "Empty Response, Error is expected when maxItems <= 0"); + + // Set skipCount = -1, maxItems = 1 + response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(-1, 1)); + Assert.assertTrue(response.isEmpty(), "Empty Response, Error is expected when skipCount < 0"); + } + + @Test(priority = 2) + public void testPagination() + { + // Search for the files under the testFolder using cmis query + String parentId = testFolder.getNodeRefWithoutVersion(); + String query = "select * from cmis:document where IN_FOLDER('" + parentId + "')"; + + // Set skipCount = 0, maxItems = 100: all 10 results fit, exact total + SearchResponse response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(0, 100)); + + // Check getTotalItems = 10, Expect hasModeItems = false + testPaginationDetails(response, 10, 0, 100); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = 0, maxItems = 10: Expect hasModeItems = false + response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(0, 10)); + + testPaginationDetails(response, 10, 0, 10); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = 0, maxItems = 5: Expect hasModeItems = true + response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(0, 5)); + + testPaginationDetails(response, 10, 0, 5); + Assert.assertTrue(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = 2, maxItems = 10: Expect hasModeItems = false + response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(2, 10)); + + testPaginationDetails(response, 10, 2, 10); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = 2, maxItems = 7: Expect hasModeItems = true + response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(2, 7)); + + testPaginationDetails(response, 10, 2, 7); + Assert.assertTrue(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = 2, maxItems = 8: Expect hasModeItems = false + response = performSearch(testUser, query, SearchLanguage.CMIS, setPaging(2, 8)); + + testPaginationDetails(response, 10, 2, 8); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + } + + @Test(priority = 3) + public void testPaginationRespectsACLs() + { + // Search for the files under the testFolder using cmis query + String parentId = testFolder.getNodeRefWithoutVersion(); + String query = "select * from cmis:document where IN_FOLDER('" + parentId + "')"; + + // Set skipCount = 0, maxItems = 100 + SearchResponse response = performSearch(testUser2, query, SearchLanguage.CMIS, setPaging(0, 100)); + + // Get getTotalItems, Expect hasModeItems = false + testPaginationDetails(response, 0, 0, 100); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = 1, maxItems = 1 + response = performSearch(testUser2, query, SearchLanguage.CMIS, setPaging(1, 1)); + + // Get getTotalItems, Expect hasModeItems = false + testPaginationDetails(response, 0, 1, 1); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + } + + @Test(priority = 4) + // https://issues.alfresco.com/jira/browse/SEARCH-2541 + public void testSearchApiPagination() + { + // Search for the files with specific title + String query = "cm:title:'" + unique_searchString + "'"; + + // Set skipCount = 0, maxItems = 100 + SearchResponse response = performSearch(testUser, query, SearchLanguage.AFTS, setPaging(0, 100)); + + // Get getTotalItems, Expect hasModeItems = false + testPaginationDetails(response, 10, 0, 100); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = 1, maxItems = 1 + response = performSearch(testUser, query, SearchLanguage.AFTS, setPaging(1, 1)); + + // Get getTotalItems, Expect hasModeItems = true + testPaginationDetails(response, 10, 1, 1); + Assert.assertTrue(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = 9, maxItems = 10 + response = performSearch(testUser, query, SearchLanguage.AFTS, setPaging(9, 10)); + + // Get getTotalItems, Expect hasModeItems = false + testPaginationDetails(response, 10, 9, 10); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = 10, maxItems = 10 + response = performSearch(testUser, query, SearchLanguage.AFTS, setPaging(10, 10)); + + // Get getTotalItems, Expect hasModeItems = false + testPaginationDetails(response, 10, 10, 10); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + } + + @Test(priority = 5) + public void testSearchApiPaginationRespectsACLs() + { + // Search for the files with specific title + String query = "cm:title:'" + unique_searchString + "'"; + + // Set skipCount = 0, maxItems = 100 + SearchResponse response = performSearch(testUser2, query, SearchLanguage.AFTS, setPaging(0, 100)); + + // Get getTotalItems, Expect hasModeItems = false + testPaginationDetails(response, 0, 0, 100); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + + // Set skipCount = 1, maxItems = 1 + response = performSearch(testUser2, query, SearchLanguage.AFTS, setPaging(1, 1)); + + // Get getTotalItems, Expect hasModeItems = false + testPaginationDetails(response, 0, 1, 1); + Assert.assertFalse(response.getPagination().isHasMoreItems(), "Incorrect: hasMoreItems"); + } + + private void testPaginationDetails(SearchResponse response, int expectedTotalCount, int skipCount, int maxItems) + { + int remaining = Math.max(0, expectedTotalCount - skipCount); + if (remaining <= maxItems) + { + Assert.assertEquals(response.getPagination().getTotalItems().intValue(), expectedTotalCount, "Unexpected document count"); + } + + Assert.assertEquals(response.getPagination().getSkipCount(), skipCount, "Unexpected skip count returned"); + Assert.assertEquals(response.getPagination().getMaxItems(), maxItems, "Unexpected maxItems returned"); + + // count = (total-skipCount) < maxItems ? total-skipCount: maxItems + int expectedCount = (expectedTotalCount < skipCount) ? 0 : Math.min((expectedTotalCount - skipCount), maxItems); + Assert.assertEquals(response.getPagination().getCount(), expectedCount, "Unexpected document count"); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchSecondaryAssociationTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchSecondaryAssociationTest.java new file mode 100644 index 0000000000..1842085f20 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchSecondaryAssociationTest.java @@ -0,0 +1,107 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.model.RestNodeAssociationModelCollection; +import org.alfresco.rest.model.RestNodeChildAssociationModel; +import org.alfresco.utility.data.CustomObjectTypeProperties; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; + +/** + * Test class tests content in the secondary parent is found too Created for Search-1313 + */ +@SuppressWarnings("PMD.OneDeclarationPerLine") +public class SearchSecondaryAssociationTest extends AbstractSearchServicesE2ETest +{ + private FolderModel testFolder1, testFolder2; + private FileModel file1; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() + { + // Folders + testFolder1 = new FolderModel("folder1"); + testFolder2 = new FolderModel("folder2"); + + // File(s) + file1 = new FileModel("file1.txt"); + file1.setContent("content file 1"); + + // Create folder1 + dataContent.usingUser(testUser).usingSite(testSite).createCustomContent(testFolder1, "cmis:folder", new CustomObjectTypeProperties()); + + // Create file1 + dataContent.usingUser(testUser).usingResource(testFolder1).createCustomContent(file1, "cmis:document", new CustomObjectTypeProperties()); + + // Create folder2 + dataContent.usingUser(testUser).usingSite(testSite).createCustomContent(testFolder2, "cmis:folder", new CustomObjectTypeProperties()); + + // wait for solr index + waitForMetadataIndexing(file1.getName(), true); + } + + @Test(priority = 1) + public void testSearchPathForSecondaryAssociation() throws Exception + { + String queryPathFolder1 = "PATH:\"/app:company_home/st:sites/cm:" + testSite.getTitle() + + "/cm:documentLibrary/cm:" + testFolder1.getName() + "/cm:" + file1.getName() + "\""; + + // Test if file can be found in folder1: Primary Parent + boolean found = isContentInSearchResults(queryPathFolder1, file1.getName(), true); + Assert.assertTrue(found, "File Not found using Primary Parent Path"); + + String queryPathFolder2 = "PATH:\"/app:company_home/st:sites/cm:" + testSite.getTitle() + + "/cm:documentLibrary/cm:" + testFolder2.getName() + "/cm:" + file1.getName() + "\""; + + // Test if file can not be found in folder2 + found = isContentInSearchResults(queryPathFolder2, file1.getName(), false); + Assert.assertTrue(found, "File found using Secondary Parent Path"); + + // Create Secondary association in folder2 + RestNodeChildAssociationModel childAssoc1 = new RestNodeChildAssociationModel(file1.getNodeRefWithoutVersion(), "cm:contains"); + restClient.authenticateUser(testUser).withCoreAPI().usingResource(testFolder2).addSecondaryChildren(childAssoc1); + + RestNodeAssociationModelCollection secondaryChildren = restClient.authenticateUser(testUser).withCoreAPI().usingResource(testFolder2).getSecondaryChildren(); + secondaryChildren.getEntryByIndex(0).assertThat().field("id").is(file1.getNodeRefWithoutVersion()); + + // Test if file can be found in folder2: Secondary Parent + found = isContentInSearchResults(queryPathFolder2, file1.getName(), true); + Assert.assertTrue(found, "File Not found using Secondary Parent Path"); + + // Remove Secondary association + restClient.authenticateUser(testUser).withCoreAPI().usingResource(testFolder2).removeSecondaryChild(secondaryChildren.getEntryByIndex(0)); + + // Test if file can not be found in folder2 + found = isContentInSearchResults(queryPathFolder2, file1.getName(), false); + Assert.assertTrue(found, "File found using Secondary Parent Path"); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchSimpleCasesTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchSimpleCasesTest.java new file mode 100644 index 0000000000..f749a6f464 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchSimpleCasesTest.java @@ -0,0 +1,53 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import org.springframework.http.HttpStatus; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +public class SearchSimpleCasesTest extends AbstractSearchServicesE2ETest +{ + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + searchServicesDataPreparation(); + waitForContentIndexing(file4.getContent(), true); + } + + @Test(priority = 1) + public void testSearchContentField() + { + SearchResponse response4 = queryUntilResponseEntriesListNotEmpty(testUser, "cm:content:unique"); + restClient.assertStatusCodeIs(HttpStatus.OK); + response4.assertThat().entriesListIsNotEmpty(); + } + + @Test(priority = 2) + public void testSearchDocxFile() + { + SearchResponse response6 = queryUntilResponseEntriesListNotEmpty(testUser, "cm:name:alfresco.docx"); + restClient.assertStatusCodeIs(HttpStatus.OK); + response6.assertThat().entriesListIsNotEmpty(); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchSpellCheckTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchSpellCheckTest.java new file mode 100644 index 0000000000..c220d4a6de --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchSpellCheckTest.java @@ -0,0 +1,416 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.dataprep.SiteService.Visibility; +import org.alfresco.rest.model.RestRequestSpellcheckModel; +import org.alfresco.utility.constants.UserRole; +import org.alfresco.utility.data.RandomData; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.SiteModel; + +/** + * Search end point Public API test with spell checking enabled. + */ +public class SearchSpellCheckTest extends AbstractSearchServicesE2ETest +{ + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + searchServicesDataPreparation(); + } + + /** + * Perform the below query { "spellcheck": {}, "query": { "userQuery": "alfrezco", "query": "cm:title:alfrezco" } } to yield a result set whose context contains a spellCheck object of type {@code searchInsteadFor} with suggestion {@code alfresco}. + */ + @Test(priority = 1) + public void testSearchMissSpelled() + { + waitForContentIndexing(file4.getContent(), true); + + // Name + SearchRequest searchReq = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("cm:name:alfrezco"); + queryReq.setUserQuery("alfrezco"); + searchReq.setQuery(queryReq); + searchReq.setSpellcheck(new RestRequestSpellcheckModel()); + assertResponse(query(searchReq)); + + // Title + queryReq.setQuery("cm:title:alfrezco"); + queryReq.setUserQuery("alfrezco"); + searchReq.setQuery(queryReq); + searchReq.setSpellcheck(new RestRequestSpellcheckModel()); + assertResponse(query(searchReq)); + + // Description + queryReq.setQuery("cm:description:alfrezco"); + queryReq.setUserQuery("alfrezco"); + searchReq.setQuery(queryReq); + searchReq.setSpellcheck(new RestRequestSpellcheckModel()); + assertResponse(query(searchReq)); + + // Content + queryReq.setQuery("cm:content:alfrezco"); + queryReq.setUserQuery("alfrezco"); + searchReq.setQuery(queryReq); + searchReq.setSpellcheck(new RestRequestSpellcheckModel()); + assertResponse(query(searchReq)); + } + + private void assertResponse(SearchResponse nodes) + { + nodes.assertThat().entriesListIsNotEmpty(); + nodes.getContext().assertThat().field("spellCheck").isNotEmpty(); + nodes.getContext().getSpellCheck().assertThat().field("suggestions").contains("alfresco"); + nodes.getContext().getSpellCheck().assertThat().field("type").is("searchInsteadFor"); + } + + /** + * Perform alternative way by setting the value in the spellcheck object. + * + *
+     * {
+     *   "query": { "query": "cm:title:alfrezco", "language": "afts" },
+     *   "spellcheck": { "query": "alfrezco" }
+     * }
+     * 
+ */ + @Test(priority = 2) + public void testSearchMissSpelledVersion2() + { + SearchRequest searchReq = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("cm:title:alfrezco"); + searchReq.setQuery(queryReq); + + RestRequestSpellcheckModel spellCheck = new RestRequestSpellcheckModel(); + spellCheck.setQuery("alfrezco"); + searchReq.setSpellcheck(spellCheck); + assertResponse(query(searchReq)); + } + + @Test(priority = 3) + public void testSearchWithSpellcheckerAndCorrectSpelling() + { + SearchRequest searchReq = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("cm:title:alfresco"); + queryReq.setUserQuery("alfresco"); + searchReq.setQuery(queryReq); + searchReq.setSpellcheck(new RestRequestSpellcheckModel()); + SearchResponse res = query(searchReq); + Assert.assertNull(res.getContext().getSpellCheck()); + res.assertThat().entriesListIsNotEmpty(); + } + + /** + * Test to check the different spellcheck types {@code searchInsteadFor} and {@code didYouMean}. Suggestion ranking is based on maxEdits, count of entries, alphabetical order. + */ + @Test(priority = 4) + public void testSpellCheckType() + { + // Create a file with the word in cm:name only + FileModel file = new FileModel("learning", "", "", FileType.TEXT_PLAIN, ""); + dataContent.usingUser(testUser).usingSite(testSite).createContent(file); + + // Wait for the file to be indexed + Assert.assertTrue(waitForMetadataIndexing(file.getName(), true)); + + // Correct spelling with cm:name field + SearchResponse response = SearchSpellcheckQuery(testUser, "cm:name:learning", "learning"); + + response.assertThat().entriesListIsNotEmpty(); + response.getContext().assertThat().field("spellCheck").isNull(); + + // Matching result, no spellcheck object returned + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, null, null); + + // Correct spelling with no specific field + response = SearchSpellcheckQuery(testUser, "learning", "learning"); + + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, null, null); + + // Correct spelling with a different field. Used cm:content field + response = SearchSpellcheckQuery(testUser, "cm:content:learning", "learning"); + + // 0 results, no spellcheck object returned + response.assertThat().entriesListIsEmpty(); + testSearchSpellcheckResponse(response, null, null); + + // Incorrect spelling with cm:name field + response = SearchSpellcheckQuery(testUser, "cm:name:lerning", "lerning"); + + // 1 match with right spelling + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + // TODO: Investigate: Share shows searchInsteadFor = lerning, API shows learning + // testSearchSpellcheckResponse(response, "searchInsteadFor", "lerning"); + + // Incorrect spelling with no field + response = SearchSpellcheckQuery(testUser, "lerning", "lerning"); + + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + // TODO: Investigate: Share shows searchInsteadFor = lerning, API shows learning + // testSearchSpellcheckResponse(response, "searchInsteadFor", "lerning"); + + // Incorrect spelling with cm:content field + response = SearchSpellcheckQuery(testUser, "cm:content:lerning", "lerning"); + + // 0 results, no spellcheck object returned + response.assertThat().entriesListIsEmpty(); + response.getContext().assertThat().field("spellCheck").isNull(); + + // Create a file with word with 1 max edit in cm:name only + FileModel file2 = new FileModel("leaning", "", "", FileType.TEXT_PLAIN, ""); + dataContent.usingUser(testUser).usingSite(testSite).createContent(file2); + + // Create a file with word in cm:name and cm:content only + FileModel file3 = new FileModel("leaning 2", "", "", FileType.TEXT_PLAIN, "leaning 2"); + dataContent.usingUser(testUser).usingSite(testSite).createContent(file3); + + Assert.assertTrue(waitForContentIndexing(file3.getContent(), true)); + + // Incorrect spelling with cm:name field + response = SearchSpellcheckQuery(testUser, "cm:name:lerning", "lerning"); + + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file2.getName()); + Assert.assertTrue(isContentInSearchResponse(response, file3.getName()), "Expected file not returned in the search results: " + file3.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "leaning"); + + // Incorrect spelling with no field + response = SearchSpellcheckQuery(testUser, "lerning", "lerning"); + + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file2.getName()); + Assert.assertTrue(isContentInSearchResponse(response, file3.getName()), "Expected file not returned in the search results: " + file3.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "leaning"); + + // Incorrect spelling with cm:content field + response = SearchSpellcheckQuery(testUser, "cm:content:lerning", "lerning"); + + Assert.assertTrue(isContentInSearchResponse(response, file3.getName()), "Expected file not returned in the search results: " + file3.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "leaning"); + + // Correct spelling with cm:name field + response = SearchSpellcheckQuery(testUser, "cm:name:learning", "learning"); + + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "didYouMean", "leaning"); + + // Correct spelling with no field + response = SearchSpellcheckQuery(testUser, "learning", "learning"); + + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "didYouMean", "leaning"); + + // Correct spelling with cm:content field + response = SearchSpellcheckQuery(testUser, "cm:content:learning", "learning"); + + Assert.assertTrue(isContentInSearchResponse(response, file3.getName()), "Expected file not returned in the search results: " + file3.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "leaning"); + } + + /** Test for the spellcheck parameters minEdit and maxPrefix. */ + @Test(priority = 5) + public void testSpellCheckParameters() + { + // Create a file with word in cm:name and cm:content + FileModel file = new FileModel("eklipse", "", "", FileType.TEXT_PLAIN, "eklipse"); + dataContent.usingUser(testUser).usingSite(testSite).createContent(file); + + // Create a file with word in cm:name, cm:title and cm:content + FileModel file2 = new FileModel("eklipses", "eklipses", "", FileType.TEXT_PLAIN, "eklipses"); + dataContent.usingUser(testUser).usingSite(testSite).createContent(file2); + + Assert.assertTrue(waitForContentIndexing(file2.getName(), true)); + + // Search with field not filled in either file + SearchResponse response = SearchSpellcheckQuery(testUser, "cm:description:'eclipse'", "eclipse"); + testSearchSpellcheckResponse(response, null, null); + + // Incorrect spelling with the field on a file as well + response = SearchSpellcheckQuery(testUser, "cm:name:'eclipse'", "eclipse"); + + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "eklipse"); + + // Incorrect spelling with no field for file1 + response = SearchSpellcheckQuery(testUser, "eclipse", "eclipse"); + + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "eklipse"); + + // Add Solr query, to check the suggestions on each shard + restClient.authenticateUser(testUser).withParams("spellcheck.q=eclipsess&spellcheck=on").withSolrAPI().getSelectQuery(); + + // Incorrect spelling with no field for file2 + response = SearchSpellcheckQuery(testUser, "eclipsess", "eclipsess"); + + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "eklipses"); + + // Search for the field only filled on file2 and not file1 + response = SearchSpellcheckQuery(testUser, "cm:title:'eclipses'", "eclipses"); + + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "eklipses"); + + // Query using 3 edits (more than spellcheck works for [maxEdits<=2]) + response = SearchSpellcheckQuery(testUser, "elapssed", "elapssed"); + testSearchSpellcheckResponse(response, null, null); + + // Query with edit on first letter (does not work with spellcheck [minPrefix=1]) + response = SearchSpellcheckQuery(testUser, "iklipse ", "iklipse "); + testSearchSpellcheckResponse(response, null, null); + } + + /** + * Test to check the fields defined for spellcheck in shared.properties work (cm:name, cm:title, cm:description and cm:content). + */ + @Test(priority = 6) + public void testSpellCheckFields() + { + // Create a file with same word in all fields + FileModel file = new FileModel("book", "book", "book", FileType.TEXT_PLAIN, "book"); + dataContent.usingUser(testUser).usingSite(testSite).createContent(file); + + Assert.assertTrue(waitForContentIndexing(file.getContent(), true)); + + // Incorrect spelling with no field + SearchResponse response = SearchSpellcheckQuery(testUser, "bo0k", "bo0k"); + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "book"); + + // Incorrect spelling with the cm:name field + response = SearchSpellcheckQuery(testUser, "cm:name:'bo0k'", "bo0k"); + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "book"); + + // Incorrect spelling with the cm:title field + response = SearchSpellcheckQuery(testUser, "cm:title:'bo0k'", "bo0k"); + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "book"); + + // Incorrect spelling with the cm:description field + response = SearchSpellcheckQuery(testUser, "cm:description:'bo0k'", "bo0k"); + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "book"); + + // Incorrect spelling with the cm:content field + response = SearchSpellcheckQuery(testUser, "cm:content:'bo0k'", "bo0k"); + Assert.assertTrue(isContentInSearchResponse(response, file.getName()), "Expected file not returned in the search results: " + file.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "book"); + + // Incorrect spelling with the cm:author field (not a suggestable field for spellcheck) + response = SearchSpellcheckQuery(testUser, "cm:author:'bo0k'", "bo0k"); + testSearchSpellcheckResponse(response, null, null); + } + + /** Test to check the ACL tracker works with spellcheck enabled. */ + @Test(priority = 7) + public void testSpellCheckACL() + { + // Create User 2 + testUser2 = dataUser.createRandomTestUser("User2"); + + // Create Private Site 2 + testSite2 = new SiteModel(RandomData.getRandomName("Site2")); + testSite2.setVisibility(Visibility.PRIVATE); + testSite2 = dataSite.usingUser(testUser).createSite(testSite2); + + // Make User 2 Site Collaborator + getDataUser().addUserToSite(testUser2, testSite2, UserRole.SiteCollaborator); + + // Add file to testSite + FileModel file1 = new FileModel("spellcheckspacebar", "", "", FileType.TEXT_PLAIN, "spellcheckspacebar"); + dataContent.usingUser(testUser).usingSite(testSite).createContent(file1); + + // Add file to testSite, testSite2 + FileModel file2 = new FileModel("spellcheckspacecar", "", "", FileType.TEXT_PLAIN, "spellcheckspacecar"); + dataContent.usingUser(testUser).usingSite(testSite).createContent(file2); + dataContent.usingUser(testUser).usingSite(testSite2).createContent(file2); + + Assert.assertTrue(waitForContentIndexing(file2.getContent(), true)); + + // Checks for User 1 + // Incorrect spelling with no field + SearchResponse response = SearchSpellcheckQuery(testUser, "spellcheckspaceber", "spellcheckspaceber"); + Assert.assertTrue(isContentInSearchResponse(response, file1.getName()), "Expected file not returned in the search results: " + file2.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "spellcheckspacebar"); + + // Correct spelling with no field + response = SearchSpellcheckQuery(testUser, "spellcheckspacebar", "spellcheckspacebar"); + Assert.assertTrue(isContentInSearchResponse(response, file1.getName()), "Expected file not returned in the search results: " + file1.getName()); + testSearchSpellcheckResponse(response, "didYouMean", "spellcheckspacecar"); + + // Incorrect spelling with no field + response = SearchSpellcheckQuery(testUser, "spellcheckspacebra", "spellcheckspacebra"); + Assert.assertTrue(isContentInSearchResponse(response, file1.getName()), "Expected file not returned in the search results: " + file1.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "spellcheckspacebar"); + + // Correct spelling with no field + response = SearchSpellcheckQuery(testUser, "spellcheckspacecar", "spellcheckspacecar"); + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file2.getName()); + testSearchSpellcheckResponse(response, null, null); + + // Add Solr query, to check the suggestions on the shard + restClient.authenticateUser(testUser).withParams("spellcheck.q=spellcheckspacebur&spellcheck=on").withSolrAPI().getSelectQuery(); + + // Checks for User 2 + // Incorrect spelling for files created with no field + response = SearchSpellcheckQuery(testUser2, "spellcheckspacebur", "spellcheckspacebur"); + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file2.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "spellcheckspacecar"); + + // Correct spelling, no field + response = SearchSpellcheckQuery(testUser2, "spellcheckspacebar", "spellcheckspacebar"); + Assert.assertFalse(isContentInSearchResponse(response, file1.getName()), "Expected file not returned in the search results: " + file1.getName()); + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file2.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "spellcheckspacecar"); + + // Incorrect spelling, no field + response = SearchSpellcheckQuery(testUser2, "spellcheckspacecra", "spellcheckspacecra"); + Assert.assertFalse(isContentInSearchResponse(response, file1.getName()), "Expected file not returned in the search results: " + file1.getName()); + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file2.getName()); + testSearchSpellcheckResponse(response, "searchInsteadFor", "spellcheckspacecar"); + + // Correct spelling, no field + response = SearchSpellcheckQuery(testUser2, "spellcheckspacecar", "spellcheckspacecar"); + Assert.assertFalse(isContentInSearchResponse(response, file1.getName()), "Expected file not returned in the search results: " + file1.getName()); + Assert.assertTrue(isContentInSearchResponse(response, file2.getName()), "Expected file not returned in the search results: " + file2.getName()); + testSearchSpellcheckResponse(response, null, null); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchTest.java new file mode 100644 index 0000000000..c37915104c --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchTest.java @@ -0,0 +1,307 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import static java.util.Arrays.asList; +import static java.util.Collections.reverse; +import static java.util.Collections.singletonList; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.testng.AssertJUnit.assertEquals; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.hamcrest.Matchers; +import org.springframework.http.HttpStatus; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.model.body.RestNodeLockBodyModel; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; + +/** + * Search end point Public API test. + */ +@SuppressWarnings({"PMD.MethodNamingConventions", "PMD.LongVariable"}) +public class SearchTest extends AbstractSearchServicesE2ETest +{ + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + searchServicesDataPreparation(); + waitForContentIndexing(file4.getContent(), true); + } + + @Test + public void searchOnIndexedData() throws Exception + { + SearchResponse nodes = query("cm:content:" + unique_searchString); + restClient.assertStatusCodeIs(HttpStatus.OK); + nodes.assertThat().entriesListIsNotEmpty(); + + SearchNodeModel entity = nodes.getEntryByIndex(0); + // MNT-25404: search/score is only populated when highlighting is requested + if (entity.getSearch() != null) + { + entity.assertThat().field("search").contains("score"); + entity.getSearch().assertThat().field("score").isNotNull(); + } + Assert.assertEquals(entity.getName(), "pangram.txt"); + } + + @Test + public void searchNonIndexedData() + { + SearchResponse nodes = query("yeti"); + restClient.assertStatusCodeIs(HttpStatus.OK); + nodes.assertThat().entriesListIsEmpty(); + } + + @Test + public void searchWithRequest() + { + SearchRequest query = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("fox"); + query.setQuery(queryReq); + query.setIncludeRequest(true); + + SearchResponse response = query(query); + restClient.assertStatusCodeIs(HttpStatus.OK); + + response.getContext().assertThat().field("request").isNotEmpty(); + } + + @Test + public void searchWithOneSortClause() + { + // Tests the ascending order first + List expectedOrder = asList("alfresco.docx", "cars.PDF", "pangram.txt"); + + SearchRequest searchRequest = createQuery("cm_name:alfresco\\.docx cm_name:cars\\.PDF cm_name:pangram\\.txt"); + searchRequest.addSortClause("FIELD", "name", true); + + RestRequestFilterQueryModel filters = new RestRequestFilterQueryModel(); + filters.setQuery("SITE:'" + testSite.getId() + "'"); + searchRequest.setFilterQueries(filters); + + SearchResponse responseWithAscendingOrder = query(searchRequest); + + restClient.assertStatusCodeIs(HttpStatus.OK); + + assertEquals( + expectedOrder, + responseWithAscendingOrder.getEntries().stream() + .map(SearchNodeModel::getModel) + .map(SearchNodeModel::getName) + .collect(Collectors.toList())); + + // Reverts the expected order... + reverse(expectedOrder); + + // ...and test the descending order + searchRequest.getSort().clear(); + searchRequest.addSortClause("FIELD", "name", false); + + SearchResponse responseWithDescendingOrder = query(searchRequest); + assertEquals( + expectedOrder, + responseWithDescendingOrder.getEntries().stream() + .map(SearchNodeModel::getModel) + .map(SearchNodeModel::getName) + .collect(Collectors.toList())); + } + + /** + * Tests the query execution with two sort clauses. The first clause has always the same value for all matches so the test makes sure the request is correctly processed and the returned order is determined by the second clause. + */ + @Test + public void searchWithTwoSortClauses() + { + // Tests the ascending order first + List expectedOrder = asList("alfresco.docx", "cars.PDF", "pangram.txt"); + + SearchRequest searchRequest = createQuery("cm_name:alfresco\\.docx cm_name:cars\\.PDF cm_name:pangram\\.txt"); + searchRequest.addSortClause("FIELD", "name", true); + searchRequest.addSortClause("FIELD", "creator", true); + + RestRequestFilterQueryModel filters = new RestRequestFilterQueryModel(); + filters.setQuery("SITE:'" + testSite.getId() + "'"); + searchRequest.setFilterQueries(filters); + + SearchResponse responseWithAscendingOrder = query(searchRequest); + + restClient.assertStatusCodeIs(HttpStatus.OK); + + assertEquals( + expectedOrder, + responseWithAscendingOrder.getEntries().stream() + .map(SearchNodeModel::getModel) + .map(SearchNodeModel::getName) + .collect(Collectors.toList())); + + // Reverts the expected order... + reverse(expectedOrder); + + // ...and test the descending order + searchRequest.getSort().clear(); + searchRequest.addSortClause("FIELD", "name", false); + searchRequest.addSortClause("FIELD", "creator", true); + + SearchResponse responseWithDescendingOrder = query(searchRequest); + assertEquals( + expectedOrder, + responseWithDescendingOrder.getEntries().stream() + .map(SearchNodeModel::getModel) + .map(SearchNodeModel::getName) + .collect(Collectors.toList())); + } + + @Test + public void searchQuery_includePermissions_shouldReturnNodeWithPermissionsInformation() + { + String query = "fox"; + String include = "permissions"; + + SearchRequest retrievalQueryIncludingPermissionsInformation = createQuery(query); + retrievalQueryIncludingPermissionsInformation.setInclude(singletonList(include)); + + query(retrievalQueryIncludingPermissionsInformation); + + restClient.assertStatusCodeIs(HttpStatus.OK); + restClient.onResponse().assertThat().body("list.entries[0].entry.permissions", notNullValue()); + + SearchRequest retrievalQueryNotIncludingLockInformation = createQuery(query); + + query(retrievalQueryNotIncludingLockInformation); + + restClient.assertStatusCodeIs(HttpStatus.OK); + restClient.onResponse().assertThat().body("list.entries[0].entry.permissions", nullValue()); + } + + @Test + public void searchQuery_includeIsLocked_shouldReturnNodeWithLockInformation() + { + String query = "fox"; + String include = "isLocked"; + + SearchRequest retrievalQueryIncludingLockInformation = createQuery(query); + retrievalQueryIncludingLockInformation.setInclude(singletonList(include)); + + query(retrievalQueryIncludingLockInformation); + + restClient.assertStatusCodeIs(HttpStatus.OK); + restClient.onResponse().assertThat().body("list.entries[0].entry.isLocked", equalTo(false)); + + RestNodeLockBodyModel lockBodyModel = new RestNodeLockBodyModel(); + lockBodyModel.setLifetime("EPHEMERAL"); + lockBodyModel.setTimeToExpire(20); + lockBodyModel.setType("FULL"); + restClient.authenticateUser(testUser).withCoreAPI().usingNode(file).lockNode(lockBodyModel); + restClient.assertStatusCodeIs(HttpStatus.OK); + + query(retrievalQueryIncludingLockInformation); + + restClient.assertStatusCodeIs(HttpStatus.OK); + restClient.onResponse().assertThat().body("list.entries[0].entry.isLocked", equalTo(true)); + + SearchRequest retrievalQueryNotIncludingLockInformation = createQuery(query); + + query(retrievalQueryNotIncludingLockInformation); + + restClient.assertStatusCodeIs(HttpStatus.OK); + restClient.onResponse().assertThat().body("list.entries[0].entry.isLocked", nullValue()); + } + + @Test + public void searchQuery_includeInvalid_shouldReturnBadResponse() + { + String query = "fox"; + String notValidInclude = "notValid"; + SearchRequest permissionsRetrieval = createQuery(query); + permissionsRetrieval.setInclude(singletonList(notValidInclude)); + + query(permissionsRetrieval); + + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST); + restClient.onResponse() + .assertThat() + .body("error.briefSummary", containsString("An invalid argument was received " + notValidInclude)); + } + + // Test that when fields parameter is set, only restricted fields appear in the response + @Test + public void searchWithFields() + { + SearchRequest query = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("alfresco"); + query.setQuery(queryReq); + + // Restrict to fields: parentId + List fields = new ArrayList<>(); + fields.add("parentId"); + query.setFields(fields); + + query(query); + restClient.assertStatusCodeIs(HttpStatus.OK); + + // Only Field parentId is included in the response + restClient.onResponse().assertThat().body("list.entries.entry[0].parentId", Matchers.notNullValue()); + // Usual Fields such as 'name, id' aren't included in the response + restClient.onResponse().assertThat().body("list.entries.entry[0].name", Matchers.nullValue()); + restClient.onResponse().assertThat().body("list.entries.entry[0].id", Matchers.nullValue()); + } + + @Test + public void searchSpecialCharacters() + { + // Create a file with Special Characters + String specialCharfileName = "è¥äæ§ç§-åæ.pdf"; + FileModel file = new FileModel(specialCharfileName, "è¥äæ§ç§-忬¯¸" + "è¥äæ§ç§-忬¯¸", "è¥äæ§ç§-忬¯¸", FileType.TEXT_PLAIN, + "Text file with Special Characters: " + specialCharfileName); + dataContent.usingUser(testUser).usingSite(testSite).createContent(file); + + waitForIndexing(file.getName(), true); + + // Search + SearchRequest searchReq = createQuery("name:'" + specialCharfileName + "'"); + SearchResponse nodes = query(searchReq); + + restClient.assertStatusCodeIs(HttpStatus.OK); + nodes.assertThat().entriesListIsNotEmpty(); + + restClient.onResponse().assertThat().body("list.entries.entry[0].name", Matchers.equalToIgnoringCase(specialCharfileName)); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchWithCustomModelTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchWithCustomModelTest.java new file mode 100644 index 0000000000..53c8c01d0e --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchWithCustomModelTest.java @@ -0,0 +1,243 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.chemistry.opencmis.commons.PropertyIds; +import org.apache.chemistry.opencmis.commons.enums.VersioningState; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.utility.constants.UserRole; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.FolderModel; + +/** + * Purpose of this TestClass is to test that the search range query tests work as expected with CustomModels Tests added for Search-1359 + */ + +public class SearchWithCustomModelTest extends AbstractSearchServicesE2ETest +{ + @Autowired + protected DataContent dataContent; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + deployCustomModel("finance-model.xml"); + deployCustomModel("tokenised-model.xml"); + + serverHealth.assertServerIsOnline(); + + dataUser.addUserToSite(testUser, testSite, UserRole.SiteContributor); + + FolderModel testFolder = dataContent.usingSite(testSite).usingUser(testUser).createFolder(); + + Long uniqueRef = System.currentTimeMillis(); + + FileModel expenseLondon = FileModel.getRandomFileModel(FileType.TEXT_PLAIN, "Expense"); + expenseLondon.setName("fin1-" + expenseLondon.getName()); + + Map properties = new HashMap<>(); + properties.put(PropertyIds.OBJECT_TYPE_ID, "D:finance:Expense"); + properties.put(PropertyIds.NAME, expenseLondon.getName()); + properties.put("finance:No", uniqueRef); + properties.put("finance:amount", 300); + + cmisApi.authenticateUser(testUser).usingSite(testSite).usingResource(testFolder).createFile(expenseLondon, properties, VersioningState.MAJOR) + .assertThat().existsInRepo(); + + // Location value is set to London + cmisApi.authenticateUser(testUser).usingResource(expenseLondon).addSecondaryTypes("P:finance:ParkEx").assertThat() + .secondaryTypeIsAvailable("P:finance:ParkEx"); + cmisApi.authenticateUser(testUser).usingResource(expenseLondon).updateProperty("finance:ParkingLocation", "London"); + + FileModel expenseParis = FileModel.getRandomFileModel(FileType.TEXT_PLAIN, "Expense"); + expenseParis.setName("fin2-" + expenseParis.getName()); + + properties = new HashMap<>(); + properties.put(PropertyIds.OBJECT_TYPE_ID, "D:finance:Expense"); + properties.put(PropertyIds.NAME, expenseParis.getName()); + properties.put("finance:No", uniqueRef + 1); + properties.put("finance:amount", 100); + properties.put("finance:Title", "Airport Taxi Outgoing"); + properties.put("finance:Emp", "David A"); + properties.put("finance:Desc", "David's Taxi"); + + cmisApi.authenticateUser(testUser).usingSite(testSite).usingResource(testFolder).createFile(expenseParis, properties, VersioningState.MAJOR) + .assertThat().existsInRepo(); + + // Location value is set to Paris + cmisApi.authenticateUser(testUser).usingResource(expenseParis).addSecondaryTypes("P:finance:ParkEx").assertThat() + .secondaryTypeIsAvailable("P:finance:ParkEx"); + cmisApi.authenticateUser(testUser).usingResource(expenseParis).updateProperty("finance:ParkingLocation", "Paris"); + + FileModel expenseNoLocation = FileModel.getRandomFileModel(FileType.TEXT_PLAIN, "receipt"); + expenseNoLocation.setName("fin3-" + expenseNoLocation.getName()); + + properties = new HashMap<>(); + properties.put(PropertyIds.OBJECT_TYPE_ID, "D:finance:Expense"); + properties.put(PropertyIds.NAME, expenseNoLocation.getName()); + properties.put("finance:No", uniqueRef + 2); + properties.put("finance:amount", 0); + properties.put("finance:Title", "Hotel Stay"); + properties.put("finance:Emp", "Daniel S"); + properties.put("finance:Desc", "Daniel's Taxi"); + + cmisApi.authenticateUser(testUser).usingSite(testSite).usingResource(testFolder).createFile(expenseNoLocation, properties, VersioningState.MAJOR) + .assertThat().existsInRepo(); + + // Location value is set to null + cmisApi.authenticateUser(testUser).usingResource(expenseNoLocation).addSecondaryTypes("P:finance:ParkEx").assertThat() + .secondaryTypeIsAvailable("P:finance:ParkEx"); + + // Wait for the file to be indexed + waitForIndexing(expenseNoLocation.getName(), true); + } + + // Search-1359: Search AFTS Query with Range + @Test(priority = 1) + public void testRangeQueryTextField() + { + // Search Range Query + SearchResponse response = queryAsUser(testUser, "finance_ParkingLocation:[* TO London]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + + // Content where Location = London is returned, If property is not set, its ignored. + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "finance_ParkingLocation:[London TO *]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + // Content where Location = London, Paris is returned, If property is not set, its ignored. + Assert.assertEquals(response.getPagination().getCount(), 2); + + response = queryAsUser(testUser, "finance_ParkingLocation:[London To Paris]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 2); + + response = queryAsUser(testUser, "finance_ParkingLocation:[* To *]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 2); + } + + @Test(priority = 2) + public void testRangeQueryTextFieldWhiteSpace() + { + SearchResponse response = queryAsUser(testUser, "finance:Emp:[* TO Daniel]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + // Tockenised field, so Returns Daniel, David A (A before Daniel) + Assert.assertEquals(response.getPagination().getCount(), 2); + + response = queryAsUser(testUser, "finance:Emp:[Dan TO *]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 2); + + response = queryAsUser(testUser, "finance:Emp:[Dan To David]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 2); + + response = queryAsUser(testUser, "finance:Emp:[* To *]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 2); + } + + @Test(priority = 3) + public void testRangeQueryTextFieldNonFacetable() + { + // Search Range Query + SearchResponse response = queryAsUser(testUser, "finance:Title:[* TO H]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "finance:Title:[Hotel TO *]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + // Tockenised field, so Includes: Hotel, (Airport) Taxi Outgoing + Assert.assertEquals(response.getPagination().getCount(), 2); + + response = queryAsUser(testUser, "finance:Title:[B To Hotel]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + // Tockenised field, so Includes: (Airport) Taxi Outgoing + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "finance:Title:[B To I]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "finance:Title:[* To *]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 2); + } + + @Test(priority = 4) + public void testRangeQueryTextFieldNotTockenised() + { + SearchResponse response = queryAsUser(testUser, "finance:Desc:[* TO David]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + // Includes: Daniel's Taxi + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "finance:Desc:[Dan TO *]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 2); + + response = queryAsUser(testUser, "finance:Desc:[Dan To David]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 1); + + response = queryAsUser(testUser, "finance:Desc:[* To *]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 2); + } + + @Test(priority = 5) + public void testRangeQueryDoubleField() + { + // Search Range Query + SearchResponse response = queryAsUser(testUser, "finance:amount:[* TO 100]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 2); + + response = queryAsUser(testUser, "finance_amount:[100 TO *]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 2); + + response = queryAsUser(testUser, "finance_amount:[100 To 300]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 2); + + response = queryAsUser(testUser, "finance_amount:[* To *]"); + restClient.assertStatusCodeIs(HttpStatus.OK); + Assert.assertEquals(response.getPagination().getCount(), 3); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/ShardInfoTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/ShardInfoTest.java new file mode 100644 index 0000000000..5dd7ae6e74 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/ShardInfoTest.java @@ -0,0 +1,245 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.collect.Sets; +import org.slf4j.Logger; +import org.springframework.http.HttpStatus; +import org.testng.SkipException; +import org.testng.annotations.Test; + +import org.alfresco.utility.LogFactory; + +/** + * Shard info end point REST API test. + */ +@SuppressWarnings({"PMD.LinguisticNaming", "PMD.UselessParentheses"}) +public class ShardInfoTest extends AbstractSearchServicesE2ETest +{ + private static final Logger LOGGER = LogFactory.getLogger(); + + /** + * Throws a TestNG SkipException when the Shard Info REST endpoint is not available on the deployment under test (e.g. Alfresco Community, which does not ship ShardInfoResource — that bean only exists in alfresco-enterprise-repo). + */ + private void skipIfShardInfoApiUnavailable() + { + String status = restClient.getStatusCode(); + if (String.valueOf(HttpStatus.NOT_FOUND.value()).equals(status)) + { + String reason = "Shard Info API is not available on alfresco-community-repo " + + "(received HTTP 404). It is provided by alfresco-enterprise-repo only. Skipping test."; + LOGGER.warn(reason); + throw new SkipException(reason); + } + } + + /* The test that will be excluded when running master slave setup, excluding the ASS_MASTER test group. */ + /* Solr-only API — shardRegistry bean is null on Elasticsearch, endpoint returns 500. */ + @Test + public void getShardInfoWithAdminAuthority() throws JsonProcessingException + { + RestShardInfoModelCollection info = restClient.authenticateUser(dataUser.getAdminUser()).withShardInfoAPI() + .getInfo(); + skipIfShardInfoApiUnavailable(); + restClient.assertStatusCodeIs(HttpStatus.OK); + info.assertThat().entriesListIsNotEmpty(); + assertEquals(info.getPagination().getTotalItems().intValue(), 2); + + List stores = Arrays.asList("workspace://SpacesStore", "archive://SpacesStore"); + List baseUrls = Arrays.asList("/solr/alfresco", "/solr/archive"); + + List entries = info.getEntries(); + for (RestShardInfoModel shardInfoModel : entries) + { + RestShardInfoModel model = shardInfoModel.getModel(); + assertEquals(model.getTemplate(), "rerank"); + assertEquals(model.getMode(), "MASTER"); + List shardingMethods = Arrays.asList("DB_ID", "DB_ID_RANGE", "EXPLICIT_ID", "ACL_ID", "MOD_ACL_ID", "DATE", "PROPERTY"); + String shardingMethod = model.getShardMethod(); + assertTrue(shardingMethods.contains(shardingMethod), "Unexpected Sharding Method Found: " + shardingMethod); + assertTrue(model.getHasContent()); + + assertTrue(stores.contains(model.getStores())); + + List shards = model.getShards(); + assertNotNull(shards); + RestShardModel shard = shards.iterator().next(); + assertNotNull(shard); + List instances = shard.getInstances(); + assertNotNull(instances); + RestInstanceModel instance = instances.iterator().next(); + assertNotNull(instance); + + assertTrue(baseUrls.contains(instance.getBaseUrl())); + + // TODO: Ideally Solr Host and Port should be Parameterised + assertNotNull(instance.getHost(), "The solr host is not present"); + assertEquals(instance.getPort().intValue(), 8983); + assertEquals(instance.getState(), "ACTIVE"); + assertEquals(instance.getMode(), "MASTER"); + } + } + + /** + * Disabled: requires a CONFIG_Master_Slave deployment (a Solr master/slave * topology). The default CI environment runs a single SOLR node, so the * assertions on mode = "MIXED" cannot hold. Re-enable manually when running * against a master/slave configuration. + */ + @Test(enabled = false) + public void getShardInfoWithAdminAuthorityMasterSlaveConfig() throws JsonProcessingException + { + RestShardInfoModelCollection info = restClient.authenticateUser(dataUser.getAdminUser()).withShardInfoAPI() + .getInfo(); + skipIfShardInfoApiUnavailable(); + restClient.assertStatusCodeIs(HttpStatus.OK); + info.assertThat().entriesListIsNotEmpty(); + assertEquals(info.getPagination().getTotalItems().intValue(), 2); + + List stores = Arrays.asList("workspace://SpacesStore", "archive://SpacesStore"); + List baseUrls = Arrays.asList("/solr/alfresco", "/solr/archive"); + + List entries = info.getEntries(); + for (RestShardInfoModel shardInfoModel : entries) + { + RestShardInfoModel model = shardInfoModel.getModel(); + assertEquals(model.getTemplate(), "rerank"); + assertEquals(model.getShardMethod(), "DB_ID"); + assertEquals(model.getMode(), "MIXED"); + assertTrue(model.getHasContent()); + + assertTrue(stores.contains(model.getStores())); + + List shards = model.getShards(); + assertNotNull(shards); + RestShardModel shard = shards.iterator().next(); + assertNotNull(shard); + List instances = shard.getInstances(); + assertNotNull(instances); + RestInstanceModel instance = instances.iterator().next(); + assertNotNull(instance); + + assertTrue(baseUrls.contains(instance.getBaseUrl())); + + assertEquals(instance.getState(), "ACTIVE"); + } + } + + @Test + public void getShardInfoWithoutAdminAuthority() throws Exception + { + restClient.authenticateUser(dataUser.createRandomTestUser()).withShardInfoAPI().getInfo(); + skipIfShardInfoApiUnavailable(); + restClient.assertStatusCodeIs(HttpStatus.INTERNAL_SERVER_ERROR); + } + + /** + * This is a test to check that sharding is correctly working on bamboo and locally. Include test group 'sharding' on bamboo to enable this test to run + * + * @throws JsonProcessingException + * Disabled: requires a CONFIG_Sharding deployment with 2 or more shards. The default CI environment runs a single shard, so * {@code numberOfShards >= 2} cannot hold. Re-enable manually when running * against a sharded configuration. + */ + @Test(enabled = false) + public void getShardInfoWith2OrMoreShards() throws JsonProcessingException + { + RestShardInfoModelCollection info = restClient.authenticateUser(dataUser.getAdminUser()).withShardInfoAPI().getInfo(); + skipIfShardInfoApiUnavailable(); + restClient.assertStatusCodeIs(HttpStatus.OK); + info.assertThat().entriesListIsNotEmpty(); + + assertEquals(info.getPagination().getTotalItems().intValue(), 2, "Pagination is: " + info.getPagination().getTotalItems().intValue() + " not expected value of 2"); + + // ... rest of method unchanged ... + Set stores = Sets.newHashSet("workspace://SpacesStore", "archive://SpacesStore"); + List baseUrls = Arrays.asList("/solr/alfresco", "/solr/archive"); + List entries = info.getEntries(); + + Set actualStores = entries.stream().map(shardInfoModel -> shardInfoModel.getModel().getStores()).collect(Collectors.toSet()); + assertEquals(actualStores, stores, "The number of stores do not match the expected number of stores"); + + for (RestShardInfoModel shardInfoModel : entries) + { + RestShardInfoModel model = shardInfoModel.getModel(); + assertEquals(model.getTemplate(), "rerank", "Template is not rerank, template found: " + model.getTemplate()); + assertEquals(model.getMode(), "MASTER", "Mode is not MASTER, mode found: " + model.getMode()); + assertTrue(model.getHasContent(), "There is no content on the shards"); + assertTrue(model.getNumberOfShards() >= 2, "Number of shards is not equal to or greater than 2"); + + List shardingMethods = Arrays.asList("DB_ID", "DB_ID_RANGE", "EXPLICIT_ID", "ACL_ID", "MOD_ACL_ID", "DATE", "PROPERTY"); + String shardingMethod = model.getShardMethod(); + assertTrue(shardingMethods.contains(shardingMethod), "Unexpected Sharding Method Found: " + shardingMethod); + + List shards = model.getShards(); + assertNotNull(shards, "There are no shards present"); + for (RestShardModel shardInstance : shards) + { + List instanceList = shardInstance.getInstances(); + for (RestInstanceModel instanceX : instanceList) + { + assertTrue(baseUrls.contains(instanceX.getBaseUrl()), "The baseUrl is not present, baseUrl found is: " + instanceX.getBaseUrl()); + assertEquals(instanceX.getState(), "ACTIVE", "Shard state is not ACTIVE, shard state is: " + instanceX.getState()); + assertNotNull(instanceX.getPort(), "There is not port found for the instance"); + assertEquals(instanceX.getMode(), "MASTER", "Mode is not MASTER, mode found: " + instanceX.getMode()); + assertTrue(instanceX.getTransactionsRemaining() >= 0, "Transactions remaining is not more than 0, transactions remaining: " + instanceX.getTransactionsRemaining()); + String shardParams = (instanceX).getShardParams(); + switch (shardingMethod) + { + case "MOD_ACL_ID": + break; + case "ACL_ID": + break; + case "DB_ID": + break; + case "DB_ID_RANGE": + assertTrue(shardParams.contains("shard.range="), "Shard Parameters Not as expected for the Shard Method: DB_ID_RANGE"); + break; + case "DATE": + assertTrue(shardParams.contains("shard.key="), "Shard Parameters Not as expected for the Shard Method: DATE"); + assertTrue(shardParams.contains("shard.date.grouping="), "Shard Parameters Not as expected for the Shard Method: DATE"); + break; + case "PROPERTY": + assertTrue(shardParams.contains("shard.key="), "Shard Parameters Not as expected for the Shard Method: PROPERTY"); + assertTrue(shardParams.contains("shard.regex="), "Shard Parameters Not as expected for the Shard Method: PROPERTY"); + break; + case "EXPLICIT_ID": + assertTrue(shardParams.contains("shard.key="), "Shard Parameters Not as expected for the Shard Method: EXPLICIT_ID"); + break; + default: + throw new AssertionError("Shard Method Not expected: " + model.getShardMethod()); + } + } + } + } + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SpecialCharacterSearchTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SpecialCharacterSearchTest.java new file mode 100644 index 0000000000..57495bcd39 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SpecialCharacterSearchTest.java @@ -0,0 +1,50 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import static org.testng.Assert.assertTrue; + +import org.testng.annotations.Test; + +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.report.Bug; + +public class SpecialCharacterSearchTest extends AbstractSearchServicesE2ETest +{ + /** + * Index a file with \u007F (delete char) in name. The goal is to check that the file is actually indexed in solr. + */ + @Test + @Bug(id = "MNT-20507") + public void testIndexDELChar() + { + FileModel file = new FileModel("Delete char\u007Ffile", FileType.TEXT_PLAIN, "content of \u007F file"); + dataContent.usingUser(testUser).usingSite(testSite).createContent(file); + assertTrue(waitForIndexing("name:'" + file.getName() + "'", true)); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/StatsSearchTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/StatsSearchTest.java new file mode 100644 index 0000000000..4d98cfa6ea --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/StatsSearchTest.java @@ -0,0 +1,309 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.http.HttpStatus; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.model.RestErrorModel; + +/** + * Stats search test. + */ +public class StatsSearchTest extends AbstractSearchServicesE2ETest +{ + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + searchServicesDataPreparation(); + waitForContentIndexing(file2.getContent(), true); + } + + @Test + public void searchWithBasicStats() + { + SearchRequest query = createQuery("cars"); + Pagination pagination = new Pagination(); + pagination.setMaxItems(2); + List statsModels = new ArrayList<>(); + RestRequestStatsModel statsModel1 = new RestRequestStatsModel(); + statsModels.add(statsModel1); + query.setStats(statsModels); + query.setPaging(pagination); + + query(query); + restClient.assertStatusCodeIs(HttpStatus.BAD_REQUEST).assertLastError() + .containsSummary(String.format(RestErrorModel.MANDATORY_PARAM, "stats field")); + + statsModel1.setField("DBID"); + SearchResponse response = query(query); + // 8 metrics by default for a numeric field + Set metricTypes = assertStatsFacetedResponse(response, "DBID", 8); + assertTrue(metricTypes.containsAll(Arrays.asList("missing", "countValues", "sum", "min", "max", "sumOfSquares", "mean", "stddev"))); + + statsModel1.setField("creator"); + response = query(query); + // 4 metrics by default for a string field + metricTypes = assertStatsFacetedResponse(response, "creator", 4); + assertTrue(metricTypes.containsAll(Arrays.asList("missing", "countValues", "min", "max"))); + + statsModel1.setField("modified"); + response = query(query); + // 8 metrics by default for a date field + metricTypes = assertStatsFacetedResponse(response, "modified", 8); + assertTrue(metricTypes.containsAll(Arrays.asList("missing", "countValues", "sum", "min", "max", "sumOfSquares", "mean", "stddev"))); + + statsModel1.setField("modifier"); + statsModel1.setMin(false); + statsModel1.setMax(false); + statsModel1.setMissing(false); + response = query(query); + metricTypes = assertStatsFacetedResponse(response, "modifier", 1); + assertFalse(metricTypes.containsAll(Arrays.asList("missing", "min", "max"))); + + statsModel1.setField("DBID"); + statsModel1.setMin(true); + statsModel1.setMax(true); + statsModel1.setCountValues(false); + statsModel1.setMissing(false); + statsModel1.setSum(false); + statsModel1.setSumOfSquares(false); + statsModel1.setMean(false); + statsModel1.setStddev(false); + response = query(query); + metricTypes = assertStatsFacetedResponse(response, "DBID", 2); + assertTrue(metricTypes.containsAll(Arrays.asList("min", "max"))); + assertFalse(metricTypes.containsAll(Arrays.asList("missing", "countValues", "sum", "sumOfSquares", "mean", "stddev"))); + + statsModel1.setField("modifier"); + statsModel1.setMin(false); + statsModel1.setMax(false); + statsModel1.setCountDistinct(true); + statsModel1.setDistinctValues(true); + statsModel1.setCardinality(true); + response = query(query); + metricTypes = assertStatsFacetedResponse(response, "modifier", 3); + assertTrue(metricTypes.containsAll(Arrays.asList("countDistinct", "distinctValues", "cardinality"))); + + statsModel1.setField("DBID"); + statsModel1.setPercentiles(Arrays.asList(1, 75, 99, 99.9)); + statsModel1.setCountDistinct(false); + statsModel1.setDistinctValues(false); + statsModel1.setCardinality(false); + response = query(query); + assertStatsFacetedResponse(response, "DBID", 1); + RestGenericMetricModel percMetric = response.getContext().getFacets().getFirst().getBuckets().getFirst().getMetrics().get(0); + assertEquals(percMetric.getType(), "percentiles"); + Map percVal = (Map) percMetric.getValue(); + Map percentiles = (Map) percVal.get("percentiles"); + assertEquals(percentiles.size(), 4); + assertTrue(percentiles.keySet().containsAll(Arrays.asList("1.0", "75.0", "99.0", "99.9"))); + } + + @Test + public void searchWithStatsLabel() + { + SearchRequest query = createQuery("cars"); + Pagination pagination = new Pagination(); + pagination.setMaxItems(2); + List statsModels = new ArrayList<>(); + RestRequestStatsModel statsModel1 = new RestRequestStatsModel(); + statsModel1.setField("modified"); + statsModel1.setLabel("DateChanged"); + statsModels.add(statsModel1); + query.setStats(statsModels); + query.setPaging(pagination); + SearchResponse response = query(query); + assertStatsFacetedResponse(response, "DateChanged", 8); + } + + @Test + public void searchWithStatsFilters() + { + SearchRequest query = new SearchRequest(); + RestRequestQueryModel queryReq = new RestRequestQueryModel(); + queryReq.setQuery("content.mimetype:text/plain"); + query.setQuery(queryReq); + List statsModels = new ArrayList<>(); + RestRequestStatsModel statsModel1 = new RestRequestStatsModel(); + statsModel1.setField("creator"); + statsModel1.setMin(false); + statsModel1.setMax(false); + statsModel1.setMissing(false); + statsModels.add(statsModel1); + query.setFilterQueries(new RestRequestFilterQueryModel("cars", Collections.singletonList("justCars"))); + query.setStats(statsModels); + SearchResponse response = query(query); + assertStatsFacetedResponse(response, "creator", 1); + RestGenericMetricModel countMetric = response.getContext().getFacets().get(0).getBuckets().get(0).getMetrics().get(0); + Integer count = response.getPagination().getTotalItems(); + Map metricCount = (Map) countMetric.getValue(); + assertEquals(count, metricCount.get("countValues")); + + statsModel1.setExcludeFilters(Collections.singletonList("justCars")); + response = query(query); + assertStatsFacetedResponse(response, "creator", 1); + countMetric = response.getContext().getFacets().get(0).getBuckets().get(0).getMetrics().get(0); + count = response.getEntries().size(); + metricCount = (Map) countMetric.getValue(); + assertTrue((Integer) metricCount.get("countValues") > count, "With the exclude filter there will be more documents than returned"); + } + + @Test + public void searchWithStatsAndMutlilevelPivot() + { + SearchRequest query = createQuery("cars"); + + Pagination pagination = new Pagination(); + pagination.setMaxItems(1); + query.setPaging(pagination); + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List list = new ArrayList<>(); + RestRequestFacetFieldModel b0 = new RestRequestFacetFieldModel("SITE"); + b0.setLabel("b0"); + list.add(b0); + list.add(new RestRequestFacetFieldModel("created")); + RestRequestFacetFieldModel b2 = new RestRequestFacetFieldModel("modifier"); + b2.setLabel("b2"); + list.add(b2); + facetFields.setFacets(list); + query.setFacetFields(facetFields); + query.setIncludeRequest(false); + + List pivotModelList = new ArrayList<>(); + RestRequestPivotModel pivots = new RestRequestPivotModel(); + pivots.setKey("b0"); + RestRequestPivotModel pivotn = new RestRequestPivotModel(); + pivotn.setKey("created"); + RestRequestPivotModel pivot2 = new RestRequestPivotModel(); + pivot2.setKey("aNumber"); + + pivotn.setPivots(Collections.singletonList(pivot2)); + pivots.setPivots(Collections.singletonList(pivotn)); + pivotModelList.add(pivots); + query.setPivots(pivotModelList); + + List statsModels = new ArrayList<>(); + RestRequestStatsModel statsModel1 = new RestRequestStatsModel(); + statsModels.add(statsModel1); + query.setStats(statsModels); + statsModel1.setField("DBID"); + statsModel1.setLabel("aNumber"); + + SearchResponse response = query(query); + response.getContext().assertThat().field("facetsFields").isNotNull(); + response.getContext().assertThat().field("facets").isNotEmpty(); + assertEquals(response.getContext().getFacetsFields().size(), 1, "There should be 1 facet field for modifier"); + assertEquals(response.getContext().getFacets().size(), 2, "There should be 1 pivot facet with stats on the end and a high level stats facet"); + + RestGenericFacetResponseModel facetResponseModel = response.getContext().getFacets().get(0); + facetResponseModel.assertThat().field("type").is("pivot"); + facetResponseModel.assertThat().field("label").is("b0"); + + // pivot created + RestGenericFacetResponseModel created = facetResponseModel.getBuckets().get(0).getFacets().get(0); + created.assertThat().field("type").is("pivot"); + created.assertThat().field("label").is("created"); + // Another nested stats + assertEquals(created.getBuckets().get(0).getMetrics().size(), 9, "Metrics are on the end of a pivot bucket"); + } + + @Test + public void searchWithStatsAndPivot() + { + SearchRequest query = createQuery("cars"); + + Pagination pagination = new Pagination(); + pagination.setMaxItems(2); + query.setPaging(pagination); + RestRequestFacetFieldsModel facetFields = new RestRequestFacetFieldsModel(); + List list = new ArrayList<>(); + list.add(new RestRequestFacetFieldModel("creator")); + facetFields.setFacets(list); + query.setFacetFields(facetFields); + query.setIncludeRequest(false); + + SearchResponse response = query(query); + response.getContext().assertThat().field("facetsFields").isNotNull(); + + List pivotModelList = new ArrayList<>(); + RestRequestPivotModel pivots = new RestRequestPivotModel(); + pivots.setKey("creator"); + RestRequestPivotModel pivotn = new RestRequestPivotModel(); + pivotn.setKey("numericId"); + + pivots.setPivots(Collections.singletonList(pivotn)); + pivotModelList.add(pivots); + query.setPivots(pivotModelList); + + List statsModels = new ArrayList<>(); + RestRequestStatsModel statsModel1 = new RestRequestStatsModel(); + statsModels.add(statsModel1); + query.setStats(statsModels); + statsModel1.setField("DBID"); + statsModel1.setLabel("numericId"); + + response = query(query); + + response.getContext().assertThat().field("facets").isNotEmpty(); + assertEquals(response.getContext().getFacets().size(), 2, "There should be 1 pivot facet with stats on the end and a high level stats facet"); + RestGenericFacetResponseModel facetResponseModel = response.getContext().getFacets().get(0); + facetResponseModel.assertThat().field("type").is("pivot"); + facetResponseModel.assertThat().field("label").is("creator"); + assertEquals(facetResponseModel.getBuckets().get(0).getMetrics().size(), 9, "Metrics are on the end of a pivot bucket"); + RestGenericFacetResponseModel statsFacet = response.getContext().getFacets().get(1); + statsFacet.assertThat().field("type").is("stats"); + statsFacet.assertThat().field("label").is("numericId"); + } + + private Set assertStatsFacetedResponse(SearchResponse response, String label, int metricsCount) + { + response.getContext().assertThat().field("facets").isNotEmpty(); + RestGenericFacetResponseModel facetResponseModel = response.getContext().getFacets().get(0); + facetResponseModel.assertThat().field("type").is("stats"); + // TODO : Change back to facetResponseModel.assertThat().field("label").is(label); + // when https://issues.alfresco.com/jira/browse/SEARCH-2125 is fixed + facetResponseModel.assertThat().field("label").contains(label); + RestGenericBucketModel bucket = facetResponseModel.getBuckets().get(0); + List metrics = bucket.getMetrics(); + assertEquals(metrics.size(), metricsCount); + return metrics.stream().map(RestGenericMetricModel::getType).collect(Collectors.toSet()); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/crosslocale/SearchExactTermCrossLocaleTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/crosslocale/SearchExactTermCrossLocaleTest.java new file mode 100644 index 0000000000..6c968a1818 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/crosslocale/SearchExactTermCrossLocaleTest.java @@ -0,0 +1,299 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search.crosslocale; + +import org.testng.annotations.Test; + +import org.alfresco.rest.search.AbstractSearchExactTermTest; + +/** + * Tests including all different tokenization (false, true, both) modes with Exact Term queries. Search Services must be configured with Cross Locale enabled in order to run these tests. These tests are based in AFTSDefaultTextQueryIT class, but an additional type of property has been added (tok:true) in order to provide full coverage for the available options. + */ +@SuppressWarnings("PMD.MethodNamingConventions") +public class SearchExactTermCrossLocaleTest extends AbstractSearchExactTermTest +{ + + /** + * Note these tests are searching in cm:name, cm:title, cm:description and cm:content properties + */ + @Test + public void exactSearch_singleTerm_shouldReturnResultsContainingExactTerm() + { + /* 2 results are expected: - Document #2 >> name: "Run", description: "you are supposed to run jump", title: "Run : a philosophy" - Document #5 >> content: "run is Good as jump" */ + assertResponseCardinality("=run", 2); + + /* No result for runner, Document #5 has "runners" in description, you can see the difference between exact search and not */ + assertResponseCardinality("=runner", 0); + assertResponseCardinality("runner", 1); + + /* 3 results are expected: - Document #2 >> description: "you are supposed to run jump", content: "after many runs you are tired and if you jump it happens the same" - Document #4 >> name: "Jump" - Document #5 >> content: "run is Good as jump" */ + assertResponseCardinality("=jump", 3); + + } + + @Test + public void exactSearch_singleTermConjunction_shouldReturnFullFieldValueMatch() + { + + /** + * Since REST API is getting the results from DB or Search Services, using single term expressions is always retrieved from DB when using default configuration "solr.query.fts.queryConsistency=TRANSACTIONAL_IF_POSSIBLE". Combining this single term with range queries (like cm:created) will ensure the results are coming from SOLR in this mode. + */ + + /* 1 result is expected for non-tokenised field (tok:false) - Document #4 >> title: "Running" */ + assertResponseCardinality("=tok:false:Running AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 1); + + /* 0 results are expected for non-tokenised field (tok:false), as there is no title: "Run" */ + assertResponseCardinality("=tok:false:Run AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 0); + + } + + /** + * These tests should be re-enabled once the following tickets have been solved: - https://alfresco.atlassian.net/browse/SEARCH-2461 - https://alfresco.atlassian.net/browse/SEARCH-2953 + */ + @Test(enabled = false) + public void failing_exactSearch_singleTerm_shouldReturnFullFieldValueMatch() + { + + // SEARCH-2953 + assertResponseCardinality("=tok:false:running AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 1); + + // SEARCH-2461 + assertResponseCardinality("=tok:false:running", 1); + + // SEARCH-2461 + assertResponseCardinality("=tok:false:Running", 1); + + // SEARCH-2461 + assertResponseCardinality("=tok:false:Run", 0); + + } + + @Test + public void exactSearch_singleTermConjunction_shouldReturnPartialFieldValueMatch() + { + + /** + * Since REST API is getting the results from DB or Search Services, using single term expressions is always retrieved from DB when using default configuration "solr.query.fts.queryConsistency=TRANSACTIONAL_IF_POSSIBLE". Combining this single term with range queries (like cm:created) will ensure the results are coming from SOLR in this mode. + */ + + /* 4 results are expected for tokenised fields (tok:true, tok:both) - Document #1 >> title: "Running jumping" - Document #3 >> title: "Running jumping twice jumpers" - Document #4 >> title: "Running" - Document #5 >> title: "Running the art of jumping" */ + assertResponseCardinality("=tok:true:running AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 4); + assertResponseCardinality("=tok:both:running AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 4); + + /** + * 4 results are expected for tokenised fields (tok:true, tok:both) - Document #1 >> title: "Running jumping" - Document #3 >> title: "Running jumping twice jumpers" - Document #4 >> title: "Running" - Document #5 >> title: "Running the art of jumping" + */ + assertResponseCardinality("=tok:true:Running AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 4); + assertResponseCardinality("=tok:both:Running AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 4); + + /** + * 1 result is expected for tokenised fields (tok:true, tok:both) - Document #2 >> title: "Run : a philosophy" + */ + assertResponseCardinality("=tok:true:Run AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 1); + assertResponseCardinality("=tok:both:Run AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 1); + + } + + /** + * These tests should be re-enabled once the following tickets have been solved: - https://alfresco.atlassian.net/browse/SEARCH-2461 + */ + @Test(enabled = false) + public void failing_exactSearch_singleTerm_shouldReturnPartialFieldValueMatch() + { + + // SEARCH-2461 + assertResponseCardinality("=tok:true:running", 4); + assertResponseCardinality("=tok:both:running", 4); + + // SEARCH-2461 + assertResponseCardinality("=tok:true:Running", 4); + assertResponseCardinality("=tok:both:Running", 4); + + // SEARCH-2461 + assertResponseCardinality("=tok:true:Run", 1); + assertResponseCardinality("=tok:both:Run", 1); + + } + + /** + * Note these tests are searching in cm:name, cm:title, cm:description and cm:content properties + */ + @Test(enabled = false) + public void exactSearch_multiTerm_shouldReturnResultsContainingExactTerm() + { + /* 3 results are expected: - Document #2 >> name: "Run", description: "you are supposed to run jump", title: "Run : a philosophy", content: "after many runs you are tired and if you jump it happens the same" - Document #4 >> name: "Jump" - Document #5 >> content: "run is Good as jump" */ + assertResponseCardinality("=run =jump", 3); + + /* No result for runner or jumper Document #3 has "jumpers" in description and title Document #5 has "runners" and "jumpers" in description You can see the difference between exact search and not */ + assertResponseCardinality("=runner =jumper", 0); + assertResponseCardinality("runner jumper", 2); + + /* 5 results are expected: - Document #1 >> name: "Running", description: "Running is a sport is a nice activity", content: "when you are running you are doing an amazing sport", title: "Running jumping" - Document #2 >> name: "Run", description: "you are supposed to run jump", content: "after many runs you are tired and if you jump it happens the same", title: "Run : a philosophy" - Document #3 >> title: "Running jumping twice jumpers" - Document #4 >> content: "runnings jumpings", title: "Running" - Document #5 >> name: "Running jumping", title: "Running the art of jumping" */ + assertResponseCardinality("=running =jumping", 5); + } + + @Test + public void exactSearch_multiTermInField_shouldReturnPartialFieldValueMatch() + { + /** + * 4 results are expected for tokenised fields (tok:true, tok:both) - Document #1 >> title: "Running jumping" - Document #3 >> title: "Running jumping twice jumpers" - Document #4 >> title: "Running" - Document #5 >> title: "Running the art of jumping" + */ + assertResponseCardinality("=tok:both:running =tok:both:jumpers AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 4); + assertResponseCardinality("=tok:true:running =tok:true:jumpers AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 4); + } + + /** + * These tests should be re-enabled once the following tickets have been solved: - https://alfresco.atlassian.net/browse/SEARCH-2461 + */ + @Test(enabled = false) + public void failing_exactSearch_multiTermInField_shouldReturnPartialFieldValueMatch() + { + + // SEARCH-2461 + assertResponseCardinality("=tok:both:running =tok:both:jumpers", 4); + assertResponseCardinality("=tok:true:running =tok:true:jumpers", 4); + + } + + /** + * These tests should be re-enabled once the following tickets have been solved: - https://alfresco.atlassian.net/browse/SEARCH-2461 - https://alfresco.atlassian.net/browse/SEARCH-2953 + */ + @Test(enabled = false) + public void failing_exactSearch_multiTermInField_shouldReturnFullFieldValueMatch() + { + + // SEARCH-2953 + assertResponseCardinality("=tok:false:running =tok:false:jumpers AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 1); + + // SEARCH-2461 + assertResponseCardinality("=tok:false:running =tok:false:jumpers", 0); + + } + + /** + * Note these tests are searching in cm:name, cm:title, cm:description and cm:content properties + */ + @Test + public void exactSearch_exactPhrase_shouldReturnResultsContainingExactPhrase() + { + /* 1 result are expected: - Document #2 >> description: "you are supposed to run jump" */ + assertResponseCardinality("=\"run jump\"", 1); + + /* No result for "runner jumper" using exact term search Document #5 has "runners" and "jumpers" in description, so it should be a result for not exact term search You can see the difference between exact search and not */ + assertResponseCardinality("=\"runner jumper\"", 0); + assertResponseCardinality("\"runner jumper\"", 1); + + /* 3 results are expected for exact term search: - Document #1 >> title: "Running jumping" - Document #3 >> title: "Running jumping twice jumpers" - Document #5 >> name: "Running jumping" + * + * When not using exact term search, 4 results are expected Since 'Milestone' wiki page (coming from ootb content) is including "running" in the content, we are checking for 5 results instead of 4 + * + * You can see the difference between exact search and not */ + assertResponseCardinality("=\"running jumping\"", 3); + assertResponseCardinality("\"running jumping\"", 5); + } + + @Test + public void exactSearch_phraseInFieldConjunction_shouldReturnFullFieldValueMatch() + { + /** + * 1 results is expected for non-tokenised field (tok:false) - Document #1 >> title: "Running jumping" + */ + assertResponseCardinality("=tok:false:\"Running jumping\" AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 1); + + /** + * No result is expected for non-tokenised field (tok:false), as there is no title: "Running jumping twice" + */ + assertResponseCardinality("=tok:false:\"Running jumping twice\" AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 0); + + } + + /** + * These tests should be re-enabled once the following tickets have been solved: - https://alfresco.atlassian.net/browse/SEARCH-2461 - https://alfresco.atlassian.net/browse/SEARCH-2953 + */ + @Test(enabled = false) + public void failing_exactSearch_phraseInFieldConjunction_shouldReturnFullFieldValueMatch() + { + + // SEARCH-2953 + assertResponseCardinality("=tok:false:\"running jumping\" AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 1); + + // SEARCH-2461 + assertResponseCardinality("=tok:false:\"running jumping\"", 1); + + // SEARCH-2461 + assertResponseCardinality("=tok:false:\"Running jumping\"", 1); + + // SEARCH-2461 + assertResponseCardinality("=tok:false:\"Running jumping twice\"", 0); + + } + + @Test + public void exactSearch_phraseInFieldConjunction_shouldReturnPartialFieldValueMatch() + { + /** + * 2 results are expected for tokenised fields (tok:true, tok:both) - Document #1 >> title: "Running jumping" - Document #3 >> title: "Running jumping twice jumpers" + */ + assertResponseCardinality("=tok:true:\"running jumping\" AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 2); + assertResponseCardinality("=tok:both:\"running jumping\" AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 2); + + /** + * 2 results are expected for tokenised fields (tok:true, tok:both) - Document #1 >> title: "Running jumping" - Document #3 >> title: "Running jumping twice jumpers" + */ + assertResponseCardinality("=tok:true:\"Running jumping\" AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 2); + assertResponseCardinality("=tok:both:\"Running jumping\" AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 2); + + /** + * 1 result is expected for tokenised fields (tok:true, tok:both) - Document #3 >> title: "Running jumping twice jumpers" + */ + assertResponseCardinality("=tok:true:\"Running jumping twice\" AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 1); + assertResponseCardinality("=tok:both:\"Running jumping twice\" AND cm:created:['" + fromDate + "' TO '" + toDate + "']", 1); + + } + + /** + * These tests should be re-enabled once the following tickets have been solved: - https://alfresco.atlassian.net/browse/SEARCH-2461 + */ + @Test(enabled = false) + public void failing_exactSearch_phraseInFieldConjunction_shouldReturnPartialFieldValueMatch() + { + + // SEARCH-2461 + assertResponseCardinality("=tok:true:\"running jumping\"", 2); + assertResponseCardinality("=tok:both:\"running jumping\"", 2); + + // SEARCH-2461 + assertResponseCardinality("=tok:true:\"Running jumping\"", 2); + assertResponseCardinality("=tok:both:\"Running jumping\"", 2); + + // SEARCH-2461 + assertResponseCardinality("=tok:true:\"Running jumping twice\"", 1); + assertResponseCardinality("=tok:both:\"Running jumping twice\"", 1); + + } + +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/fingerprint/FingerPrintTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/fingerprint/FingerPrintTest.java new file mode 100644 index 0000000000..0f4b1dd49e --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/fingerprint/FingerPrintTest.java @@ -0,0 +1,239 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search.fingerprint; + +import static com.google.common.collect.Sets.newHashSet; +import static org.testng.Assert.assertTrue; + +import java.util.Set; + +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.search.AbstractE2EFunctionalTest; +import org.alfresco.rest.search.SearchResponse; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.report.Bug; + +/** + * Search end point Public API test with fingerprint. + */ +@SuppressWarnings({"PMD.OneDeclarationPerLine", "PMD.SingularField"}) +public class FingerPrintTest extends AbstractE2EFunctionalTest +{ + private FolderModel folder; + private FileModel fileBanana, fileTaco, fileCat, fileDog, fileOriginal; + /** + * File to be created in dataprep with same content as fileOriginal File to be updated in test searchAfterVersionUpdate File to be reverted in test searchAfterVersionRevert + */ + private FileModel fileToBeUpdated; + + @BeforeClass(alwaysRun = true) + public void dataPreparation() throws Exception + { + /* Create the following file structure in the same Site : In addition to the preconditions created in dataPreparation |- folder |-- pangram-banana.txt |-- pangram-taco.txt |-- pangram-cat.txt |-- pangram-dog.txt |-- original.txt |-- toBeUpdated.txt */ + folder = new FolderModel("The quick brown fox jumps over"); + dataContent.usingUser(testUser).usingSite(testSite).createFolder(folder); + + fileBanana = new FileModel("pangram-banana.txt", FileType.TEXT_PLAIN, "The quick brown fox jumps over the lazy banana"); + fileTaco = new FileModel("pangram-taco.txt", FileType.TEXT_PLAIN, "The quick brown fox jumps over the lazy dog that ate the taco"); + + fileCat = new FileModel("pangram-cat.txt", FileType.TEXT_PLAIN, "The quick brown fox jumps over the lazy cat"); + fileDog = new FileModel("pangram-dog.txt", FileType.TEXT_PLAIN, "The quick brown fox ate the lazy dog"); + + // fileOriginal with different content + fileOriginal = new FileModel("original.txt", FileType.TEXT_PLAIN, "This is an original file with some content"); + + // Index a new fileToBeUpdated with content similar to fileOriginal + fileToBeUpdated = new FileModel("toBeUpdated.txt", FileType.TEXT_PLAIN, "This is an original file with some content"); + + dataContent.usingUser(testUser).usingSite(testSite).usingResource(folder).createContent(fileBanana); + dataContent.usingUser(testUser).usingSite(testSite).usingResource(folder).createContent(fileTaco); + dataContent.usingUser(testUser).usingSite(testSite).usingResource(folder).createContent(fileCat); + dataContent.usingUser(testUser).usingSite(testSite).usingResource(folder).createContent(fileDog); + + dataContent.usingUser(testUser).usingSite(testSite).usingResource(folder).createContent(fileOriginal); + dataContent.usingUser(testUser).usingSite(testSite).usingResource(folder).createContent(fileToBeUpdated); + + // Additional wait implemented to remove inconsistent failures. Ref: Search-1438 for details + waitForIndexing("FINGERPRINT:" + fileDog.getNodeRefWithoutVersion(), true); + waitForIndexing("FINGERPRINT:" + fileToBeUpdated.getNodeRefWithoutVersion(), true); + } + + @Test(priority = 1) + @Bug(id = "MNT-20449") + public void makeSureFingerprintQueryWorksAfterMetadataUpdate() + { + // Index a new file with content + FileModel file = new FileModel("Project_Contract.txt", FileType.TEXT_PLAIN, "A content which is completely different from other indexed files."); + dataContent.usingUser(testUser).usingSite(testSite).usingResource(folder).createContent(file); + + // make sure the content has been indexed (i.e. the ContentTracker fingerprint has been correctly computed + assertTrue(waitForIndexing("FINGERPRINT:" + file.getNodeRefWithoutVersion(), true)); + + // Update some metadata attribute of the file + String newFileName = "Contrattazione.pdf"; + file.setName(newFileName); + + // ...and reindex it + dataContent.usingUser(testUser).usingSite(testSite).usingResource(file).renameContent(file); + + // Make sure the new version of the file has been indexed + assertTrue(waitForMetadataIndexing("Contrattazione", true)); + + // ...and finally + assertTrue(waitForIndexing("FINGERPRINT:" + file.getNodeRefWithoutVersion(), true)); + } + + /** + * Search similar document based on document finger print. The data prep should have loaded 2 files which one is similar to the files loaded as part of this test. Note that for fingerprint to work it need a 5 word sequence. + */ + @Test(priority = 2) + public void search() + { + String uuid = fileBanana.getNodeRefWithoutVersion(); + Assert.assertNotNull(uuid); + + String fingerprintQuery = String.format("FINGERPRINT:%s", uuid); + SearchResponse response = queryAsUser(testUser, fingerprintQuery); + + int count = response.getEntries().size(); + assertTrue(count > 1); + + Set expectedNames = newHashSet(); + expectedNames.add(fileBanana.getName()); + expectedNames.add(fileTaco.getName()); + expectedNames.add(fileCat.getName()); + + testSearchQueryUnordered(fingerprintQuery, expectedNames, SearchLanguage.AFTS); + } + + @Test(priority = 3) + public void searchSimilar() + { + String uuid = fileTaco.getNodeRefWithoutVersion(); + Assert.assertNotNull(uuid); + + // In the response entity there is a score of each doc, change below threshold to bring more like or less. + String fingerprintQuery = String.format("FINGERPRINT:%s_68", uuid); + SearchResponse response = query(fingerprintQuery); + + int count = response.getEntries().size(); + assertTrue(count >= 1); + + Set expectedNames = newHashSet(); + expectedNames.add(fileTaco.getName()); + + testSearchQueryUnordered(fingerprintQuery, expectedNames, SearchLanguage.AFTS); + } + + @Test(priority = 4) + public void searchSimilar67Percent() + { + String uuid = fileTaco.getNodeRefWithoutVersion(); + Assert.assertNotNull(uuid); + + // Check that: When asked for less overlap of 67%: More documents are matched + String fingerprintQuery = String.format("FINGERPRINT:%s_67", uuid); + SearchResponse response = query(fingerprintQuery); + + int count = response.getEntries().size(); + assertTrue(count > 1); + + Set expectedNames = newHashSet(); + expectedNames.add(fileTaco.getName()); + expectedNames.add(fileBanana.getName()); + expectedNames.add(fileCat.getName()); + + testSearchQueryUnordered(fingerprintQuery, expectedNames, SearchLanguage.AFTS); + } + + @Test(priority = 5) + @Bug(id = "SEARCH-2065") + public void searchAfterVersionUpdate() + { + // Check that fileToBeUpdated is found with a fingerprint query with fileOriginal: as they have similar content + boolean found = isContentInSearchResults("FINGERPRINT:" + fileOriginal.getNodeRefWithoutVersion(), fileToBeUpdated.getName(), true); + Assert.assertTrue(found, "Matching File Not found in results for Fingerprint Query with original file"); + + // Update content of the fileToBeUpdated to match file1 in the dataprep + String newFileContent = "The quick brown fox jumps over the updated file"; + fileToBeUpdated.setContent(newFileContent); + dataContent.usingUser(testUser).usingSite(testSite).usingResource(fileToBeUpdated).updateContent(newFileContent); + + // Wair for the new version of the file to be indexed + assertTrue(waitForContentIndexing(fileToBeUpdated.getContent(), true)); + + // Check that fileToBeUpdated is NOT found with a fingerprint query with fileOriginal + boolean notFound = isContentInSearchResults("FINGERPRINT:" + fileOriginal.getNodeRefWithoutVersion(), fileToBeUpdated.getName(), false); + Assert.assertTrue(notFound, "Updated File unexpectedly found in results for Fingerprint Query with original file"); + + // Check that fileToBeUpdated is found with a fingerprint query with file1 + found = isContentInSearchResults("FINGERPRINT:" + fileBanana.getNodeRefWithoutVersion(), fileToBeUpdated.getName(), true); + Assert.assertTrue(found, "Update File Not found in results for Fingerprint Query with updated content"); + } + + @Test(priority = 6) + @Bug(id = "SEARCH-2065") + public void searchAfterVersionRevert() + { + // Revert fileToBeUpdated to previous version + restClient.authenticateUser(testUser).withCoreAPI().usingNode(fileToBeUpdated).revertVersion("1.0", "{}"); + String revertedContent = restClient.authenticateUser(testUser).withCoreAPI().usingNode(fileToBeUpdated).getVersionContent("1.2").getResponse().asString(); + Assert.assertEquals(revertedContent, fileOriginal.getContent(), "Reverted content does not match Original"); + + // Wair for the new version of the file to be indexed + assertTrue(waitForContentIndexing(fileToBeUpdated.getContent(), true)); + + // Check that fileToBeUpdated is found with a fingerprint query with fileOriginal + boolean found = isContentInSearchResults("FINGERPRINT:" + fileOriginal.getNodeRefWithoutVersion(), fileToBeUpdated.getName(), true); + Assert.assertTrue(found, "File not found in results for Fingerprint Query with original file after reverting version changes"); + + // Check that fileToBeUpdated is NOT found with a fingerprint query with file1 + boolean notFound = isContentInSearchResults("FINGERPRINT:" + fileBanana.getNodeRefWithoutVersion(), fileToBeUpdated.getName(), false); + Assert.assertTrue(notFound, "File appears in the results for Fingerprint Query even after reverting content changes"); + } + + @Test(priority = 7) + @Bug(id = "SEARCH-2065") + public void searchAfterVersionDelete() + { + // Revert fileToBeUpdated to previous version + restClient.authenticateUser(testUser).withCoreAPI().usingNode(fileToBeUpdated).deleteNodeVersion("1.2"); + + // Check that fileToBeUpdated is NOT found with a fingerprint query with fileOriginal + boolean notFound = isContentInSearchResults("FINGERPRINT:" + fileOriginal.getNodeRefWithoutVersion(), fileToBeUpdated.getName(), false); + Assert.assertTrue(notFound, "Updated File unexpectedly found in results for Fingerprint Query with original file"); + + // Check that fileToBeUpdated is found with a fingerprint query with file1 + boolean found = isContentInSearchResults("FINGERPRINT:" + fileBanana.getNodeRefWithoutVersion(), fileToBeUpdated.getName(), true); + Assert.assertTrue(found, "Update File Not found in results for Fingerprint Query with updated content"); + } +} diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/tracker/CascadingIntegrationTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/tracker/CascadingIntegrationTest.java new file mode 100644 index 0000000000..ce5f1552c8 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/tracker/CascadingIntegrationTest.java @@ -0,0 +1,271 @@ +/* + * #%L + * Alfresco Search Services E2E Test + * %% + * Copyright (C) 2005 - 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.rest.search.tracker; + +import static org.testng.Assert.assertTrue; + +import java.util.List; +import java.util.Map; + +import org.apache.chemistry.opencmis.commons.PropertyIds; +import org.apache.chemistry.opencmis.commons.enums.VersioningState; +import org.springframework.beans.factory.annotation.Autowired; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.alfresco.rest.search.AbstractE2EFunctionalTest; +import org.alfresco.utility.data.DataContent; +import org.alfresco.utility.model.ContentModel; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FileType; +import org.alfresco.utility.model.FolderModel; + +/** + * Test class tests cascading updates for a child node when parent node is updated + */ +public class CascadingIntegrationTest extends AbstractE2EFunctionalTest +{ + @Autowired + protected DataContent dataContent; + private FolderModel parentFolderSharded; + private FileModel firstChildFileSharded; + private FileModel secondChildFileSharded; + + @BeforeClass(alwaysRun = true) + public void setupEnvironment() + { + assertTrue(deployCustomModel("sharding-content-model.xml"), + "failing while deploying sharding model"); + } + + @AfterClass + public void cleanUpEnvironment() + { + if (firstChildFileSharded != null) + { + dataContent.usingSite(testSite).usingUser(testUser).usingResource(firstChildFileSharded).deleteContent(); + restClient.withCoreAPI().usingTrashcan().deleteNodeFromTrashcan(firstChildFileSharded); + } + + if (secondChildFileSharded != null) + { + dataContent.usingSite(testSite).usingUser(testUser).usingResource(secondChildFileSharded).deleteContent(); + restClient.withCoreAPI().usingTrashcan().deleteNodeFromTrashcan(secondChildFileSharded); + } + + if (parentFolderSharded != null) + { + dataContent.usingSite(testSite).usingUser(testUser).usingResource(parentFolderSharded).deleteContent(); + restClient.withCoreAPI().usingTrashcan().deleteNodeFromTrashcan(parentFolderSharded); + } + + dataContent.deleteSite(testSite); + assertTrue(deactivateCustomModel("sharding-content-model.xml"), + "failing while deactivating sharding model"); + assertTrue(deleteCustomModel("sharding-content-model.xml"), + "failing while removing sharding model"); + } + + @Test(priority = 1) + public void testChildPathWhenParentRenamed() + { + // Create Parent folder + FolderModel parentFolder = dataContent.usingSite(testSite).usingUser(testUser).createFolder(); + + // Create a file in the parent folder + FileModel childFile = FileModel.getRandomFileModel(FileType.TEXT_PLAIN, "custom content"); + + cmisApi.authenticateUser(testUser).usingSite(testSite).usingResource(parentFolder) + .createFile(childFile, + Map.of(PropertyIds.NAME, childFile.getName(), + PropertyIds.OBJECT_TYPE_ID, "cmis:document"), + VersioningState.MAJOR) + .assertThat().existsInRepo(); + + // Query to find nodes where Path with original folder name matches + String parentQuery = "PATH:\"/app:company_home/st:sites/cm:" + testSite.getTitle() + + "/cm:documentLibrary/cm:" + parentFolder.getName() + "/*\""; + + // Rename parent folder + String parentNewName = "parentRenamed"; + parentFolder.setName(parentNewName); + + ContentModel parentNewNameModel = new ContentModel(parentNewName); + dataContent.usingUser(testUser).usingResource(parentFolder).renameContent(parentNewNameModel); + + waitForMetadataIndexing(parentNewName, true); + + // Find nodes where Path with new folder name matches + String parentQueryAfterRename = "PATH:\"/app:company_home/st:sites/cm:" + testSite.getTitle() + + "/cm:documentLibrary/cm:" + parentNewName + "/*\""; + boolean indexingInProgress = !isContentInSearchResults(parentQueryAfterRename, childFile.getName(), true); + + // Query using new parent name: Expect child file + int descendantCountOfNewName = query(parentQueryAfterRename).getPagination().getCount(); + Assert.assertEquals(descendantCountOfNewName, 1, String.format("Indexing in progress: %s New renamed path has not the same descendants as before renaming: %s", indexingInProgress, parentQueryAfterRename)); + + // Query using old parent name: Expect no descendant after rename + int descendantCountOfOriginalName = query(parentQuery).getPagination().getCount(); + Assert.assertEquals(descendantCountOfOriginalName, 0, "Old path still has descendants: " + parentQuery); + } + + @Test(priority = 2) + public void testGrandChildPathWhenGrandParentRenamed() + { + // Create grandparent folder + FolderModel grandParentFolder = dataContent.usingSite(testSite).usingUser(testUser).createFolder(); + + // Create child folder + FolderModel childFolder = dataContent.usingUser(testUser).usingResource(grandParentFolder).createFolder(); + + // Create grand child file + FileModel grandChildFile = FileModel.getRandomFileModel(FileType.TEXT_PLAIN, "custom content"); + + cmisApi.authenticateUser(testUser).usingSite(testSite).usingResource(childFolder) + .createFile(grandChildFile, + Map.of(PropertyIds.NAME, grandChildFile.getName(), + PropertyIds.OBJECT_TYPE_ID, "cmis:document"), + VersioningState.MAJOR) + .assertThat().existsInRepo(); + + // Wait for file to be indexed + waitForMetadataIndexing(grandChildFile.getName(), true); + + // Query to find nodes where Path with original folder name matches + + String parentQuery = "PATH:\"/app:company_home/st:sites/cm:" + testSite.getTitle() + + "/cm:documentLibrary/cm:" + grandParentFolder.getName() + "/*\""; + + // Rename grand parent folder + String grandParentNewName = "grandParentRenamed"; + grandParentFolder.setName(grandParentNewName); + + ContentModel grandParentFolderRenamed = new ContentModel(grandParentNewName); + dataContent.usingUser(testUser).usingResource(grandParentFolder).renameContent(grandParentFolderRenamed); + + // Find nodes where Path with new folder name matches + String childrenQueryAfterRename = "PATH:\"/app:company_home/st:sites/cm:" + testSite.getTitle() + + "/cm:documentLibrary/cm:" + grandParentNewName + "/*\""; + + String grandChildrenQueryAfterRename = "PATH:\"/app:company_home/st:sites/cm:" + testSite.getTitle() + + "/cm:documentLibrary/cm:" + grandParentNewName + "/cm:" + childFolder.getName() + "/*\""; + + boolean indexingInProgress = !isContentInSearchResults(grandChildrenQueryAfterRename, grandChildFile.getName(), true); + + // Query using new parent name: Expect grandchild file + int grandChildrenCountOfNewName = query(grandChildrenQueryAfterRename).getPagination().getCount(); + Assert.assertEquals(grandChildrenCountOfNewName, 1, + String.format("Indexing in progress: %s New renamed path has not the same descendants as before renaming: %s", + indexingInProgress, + grandChildrenQueryAfterRename)); + + // Query using new parent name: Expect child folder + int childrenCountOfNewName = query(childrenQueryAfterRename).getPagination().getCount(); + Assert.assertEquals(childrenCountOfNewName, 1, + String.format("Indexing in progress: %s New renamed path has not the same descendants as before renaming: %s", + indexingInProgress, + childrenQueryAfterRename)); + + // Query using old parent name: Expect no descendant after rename + int descendantCountOfOriginalName = query(parentQuery).getPagination().getCount(); + Assert.assertEquals(descendantCountOfOriginalName, 0, "Old path still has descendants: " + parentQuery); + } + + /** + * Index three nodes (parent folder and two files) in two different shards. Check that, after parent renaming, both the children are searchable in the new path (computed accordingly with the new parent folder name) + */ + @Test(priority = 3) + @SuppressWarnings("PMD.LongVariable") + public void testChildrenPathOnParentRenamedWithChildrenInDifferentShards() + { + + // Create Parent folder. It will be indexed in shard 0 + parentFolderSharded = FolderModel.getRandomFolderModel(); + + List secondaryTypes = List.of("P:shard:sharding"); + Map parentProperties = Map.of(PropertyIds.NAME, parentFolderSharded.getName(), + PropertyIds.OBJECT_TYPE_ID, "cmis:folder", + "cmis:secondaryObjectTypeIds", secondaryTypes, + "shard:shardId", "0"); + + // Create a first child in parent folder. It will be indexed in the parent shard (shard 0) + firstChildFileSharded = FileModel.getRandomFileModel(FileType.TEXT_PLAIN, "custom content"); + Map propertiesFirstChild = Map.of(PropertyIds.NAME, firstChildFileSharded.getName(), + PropertyIds.OBJECT_TYPE_ID, "cmis:document", + "cmis:secondaryObjectTypeIds", secondaryTypes, + "shard:shardId", "0"); + + // Create a second child in parent folder. It will be indexed in shard 1. + secondChildFileSharded = FileModel.getRandomFileModel(FileType.TEXT_PLAIN, "custom content"); + Map propertiesSecondChild = Map.of(PropertyIds.NAME, secondChildFileSharded.getName(), + PropertyIds.OBJECT_TYPE_ID, "cmis:document", + "cmis:secondaryObjectTypeIds", secondaryTypes, + "shard:shardId", "1"); + + cmisApi.authenticateUser(testUser).usingSite(testSite).createFolder(parentFolderSharded, parentProperties).then() + .usingResource(parentFolderSharded) + .createFile(firstChildFileSharded, propertiesFirstChild, VersioningState.MAJOR) + .createFile(secondChildFileSharded, propertiesSecondChild, VersioningState.MAJOR); + + // Check everything is indexed + assertTrue(waitForIndexing(firstChildFileSharded.getName(), true), "file: " + firstChildFileSharded.getName() + " has not been indexed."); + assertTrue(waitForIndexing(secondChildFileSharded.getName(), true), "file: " + secondChildFileSharded.getName() + " has not been indexed."); + assertTrue(waitForIndexing(parentFolderSharded.getName(), true), "file: " + parentFolderSharded.getName() + " has not been indexed."); + + // Query to find nodes where Path with original folder name matches + String parentQuery = "PATH:\"/app:company_home/st:sites/cm:" + testSite.getTitle() + + "/cm:documentLibrary/cm:" + parentFolderSharded.getName() + "/*\""; + + // Rename parent folder + String parentNewName = "parentRenamedSharding"; + parentFolderSharded.setName(parentNewName); + ContentModel parentNewNameModel = new ContentModel(parentNewName); + dataContent.usingUser(testUser).usingResource(parentFolderSharded).renameContent(parentNewNameModel); + + String parentQueryAfterRename = "PATH:\"/app:company_home/st:sites/cm:" + testSite.getTitle() + + "/cm:documentLibrary/cm:" + parentNewName + "/*\""; + + // Query using new parent name: Expect the two children + int descendantCountOfNewNameBeforeUpdate = query(parentQueryAfterRename).getPagination().getCount(); + Assert.assertEquals(descendantCountOfNewNameBeforeUpdate, 0, "There should be 0 results performing the new query before updating parent name"); + + assertTrue(waitForMetadataIndexing(parentNewName, true), "failing while renaming " + parentFolderSharded.getName() + " to " + parentNewName); + + boolean indexingInProgress = !isContentInSearchResults(parentQueryAfterRename, firstChildFileSharded.getName(), true); + + // Query using new parent name: Expect the two children + int descendantCountOfNewName = query(parentQueryAfterRename).getPagination().getCount(); + Assert.assertEquals(descendantCountOfNewName, 2, String.format("Indexing in progress: %s New renamed path has not the same descendants as before renaming: %s", indexingInProgress, parentQuery)); + + // Query using old parent name: Expect no descendant after rename + int descendantCountOfOriginalName = query(parentQuery).getPagination().getCount(); + Assert.assertEquals(descendantCountOfOriginalName, 0, "Old path still has descendants: " + parentQuery); + + } +} diff --git a/packaging/tests/tas-restapi/src/test/resources/default.properties b/packaging/tests/tas-restapi/src/test/resources/default.properties index 60d58a5b73..52a243dee8 100644 --- a/packaging/tests/tas-restapi/src/test/resources/default.properties +++ b/packaging/tests/tas-restapi/src/test/resources/default.properties @@ -8,10 +8,12 @@ sync.scheme=http sync.server=localhost sync.port=9090 -# Solr Server Settings +# Solr Server Settings. solr.scheme=http solr.server=localhost solr.port=8083 +solr.secret=secret +solr.secret.name=X-Alfresco-Search-Secret #Solr Indexing Time solrWaitTimeInSeconds=20 @@ -20,6 +22,16 @@ solrWaitTimeInSeconds=20 admin.user=admin admin.password=admin +# CMIS settings - required by search E2E tests +cmis.binding=browser +cmis.basePath=/alfresco/api/-default-/public/cmis/versions/1.1/${cmis.binding} + +# ActiveMQ - required by TAS dataprep +messaging.broker.url=failover:(tcp://localhost:61616)?timeout=3000 +messaging.broker.ssl=false +messaging.broker.username=admin +messaging.broker.password=admin + # Identity Service configuration # set this property to run tests using AIS # identity-service.auth-server-url=http://localhost:8999/auth diff --git a/packaging/tests/tas-restapi/src/test/resources/elasticsearch-e2e-suite.xml b/packaging/tests/tas-restapi/src/test/resources/elasticsearch-e2e-suite.xml new file mode 100644 index 0000000000..fc5b30c26b --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/resources/elasticsearch-e2e-suite.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packaging/tests/tas-restapi/src/test/resources/finance-model.xml b/packaging/tests/tas-restapi/src/test/resources/finance-model.xml new file mode 100644 index 0000000000..67799651d2 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/resources/finance-model.xml @@ -0,0 +1,149 @@ + + + Administrator + + + + + + + + + + + + Receipt + cm:content + + + ReceiptValue + d:double + false + + false + true + + + + ReceiptNo + d:long + false + + true + + + + + + + + + Expense + cm:content + + + No + d:long + false + + true + true + + + + Emp + d:text + false + + true + true + + + + Amount + d:double + false + + true + true + + + + Title + d:text + false + + both + false + + + + Description + d:text + false + + false + false + + + + CreatedAt + d:datetime + false + + true + + + + Location + d:text + true + + true + true + + + + EmpNo + d:int + false + + true + true + + + + EmpDept + d:float + false + + true + true + + + + + + + + + + + Parking Expense + + + Location + d:text + true + + true + true + + + + + + + + + \ No newline at end of file diff --git a/packaging/tests/tas-restapi/src/test/resources/indexing-disabled-content-model.xml b/packaging/tests/tas-restapi/src/test/resources/indexing-disabled-content-model.xml new file mode 100644 index 0000000000..3cabaf650d --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/resources/indexing-disabled-content-model.xml @@ -0,0 +1,32 @@ + + + Indexing Disabled Content Model for Non-Indexed Fields E2E Tests + Alfresco + + + + + + + + + + Sample + cm:content + + + Indexed Field + d:text + false + true + + + Non-Indexed Field + d:text + false + + + + + + \ No newline at end of file diff --git a/packaging/tests/tas-restapi/src/test/resources/search-2139-model.xml b/packaging/tests/tas-restapi/src/test/resources/search-2139-model.xml new file mode 100644 index 0000000000..a19aa3334c --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/resources/search-2139-model.xml @@ -0,0 +1,74 @@ + + + All Field Types Model for Search Field Definition Tests (SEARCH-2139) + Alfresco + + + + + + + + + + All Field Types Text Aspect + + + Text LOV Partial + d:text + false + true + + + Text Pattern Unique + d:text + false + true + + + Text Free + d:text + false + true + + + Text Pattern Many + d:text + false + true + + + Text LOV Whole (not tokenised) + d:text + false + false + + + MLText LOV Partial + d:mltext + false + true + + + MLText Free + d:mltext + false + true + + + MLText LOV Whole (not tokenised) + d:mltext + false + false + + + Multiple MLText + d:mltext + false + true + true + + + + + \ No newline at end of file diff --git a/packaging/tests/tas-restapi/src/test/resources/search-e2e-crosslocale-suite.xml b/packaging/tests/tas-restapi/src/test/resources/search-e2e-crosslocale-suite.xml new file mode 100644 index 0000000000..4398da08b6 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/resources/search-e2e-crosslocale-suite.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packaging/tests/tas-restapi/src/test/resources/sharding-content-model.xml b/packaging/tests/tas-restapi/src/test/resources/sharding-content-model.xml new file mode 100644 index 0000000000..0cc8091979 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/resources/sharding-content-model.xml @@ -0,0 +1,27 @@ + + + Sharding Content Model for Explicit Routing E2E Tests + Alfresco + + + + + + + + + + Sharding + + + Shard ID + d:text + false + + false + + + + + + \ No newline at end of file diff --git a/packaging/tests/tas-restapi/src/test/resources/solr-search-e2e-fingerprint-suite.xml b/packaging/tests/tas-restapi/src/test/resources/solr-search-e2e-fingerprint-suite.xml new file mode 100644 index 0000000000..feca9c20ac --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/resources/solr-search-e2e-fingerprint-suite.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packaging/tests/tas-restapi/src/test/resources/solr-search-e2e-spellcheck-suite.xml b/packaging/tests/tas-restapi/src/test/resources/solr-search-e2e-spellcheck-suite.xml new file mode 100644 index 0000000000..d0757c0bdf --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/resources/solr-search-e2e-spellcheck-suite.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packaging/tests/tas-restapi/src/test/resources/test-suites/part2-suite.xml b/packaging/tests/tas-restapi/src/test/resources/test-suites/part2-suite.xml index de3460385f..14e5417245 100644 --- a/packaging/tests/tas-restapi/src/test/resources/test-suites/part2-suite.xml +++ b/packaging/tests/tas-restapi/src/test/resources/test-suites/part2-suite.xml @@ -15,7 +15,15 @@ - + + + + + diff --git a/packaging/tests/tas-restapi/src/test/resources/tokenised-model.xml b/packaging/tests/tas-restapi/src/test/resources/tokenised-model.xml new file mode 100644 index 0000000000..5c351f0eb2 --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/resources/tokenised-model.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + cm:content + + + d:text + + true + + + + d:text + + false + + + + d:text + + both + + + + + + + \ No newline at end of file diff --git a/repository/src/test/resources/search/model/finance-model.xml b/repository/src/test/resources/search/model/finance-model.xml new file mode 100644 index 0000000000..67799651d2 --- /dev/null +++ b/repository/src/test/resources/search/model/finance-model.xml @@ -0,0 +1,149 @@ + + + Administrator + + + + + + + + + + + + Receipt + cm:content + + + ReceiptValue + d:double + false + + false + true + + + + ReceiptNo + d:long + false + + true + + + + + + + + + Expense + cm:content + + + No + d:long + false + + true + true + + + + Emp + d:text + false + + true + true + + + + Amount + d:double + false + + true + true + + + + Title + d:text + false + + both + false + + + + Description + d:text + false + + false + false + + + + CreatedAt + d:datetime + false + + true + + + + Location + d:text + true + + true + true + + + + EmpNo + d:int + false + + true + true + + + + EmpDept + d:float + false + + true + true + + + + + + + + + + + Parking Expense + + + Location + d:text + true + + true + true + + + + + + + + + \ No newline at end of file diff --git a/scripts/ci/build.sh b/scripts/ci/build.sh index 6b25a69374..da87a457b7 100644 --- a/scripts/ci/build.sh +++ b/scripts/ci/build.sh @@ -14,6 +14,10 @@ else PROFILES="-Pags" fi +if [[ "${REQUIRES_TAS_TESTS}" == "true" ]]; then + PROFILES="${PROFILES} -Pall-tas-tests" +fi + if [[ "${REQUIRES_INSTALLED_ARTIFACTS}" == "true" ]]; then PHASE="install" else