ACS-11962 Migrate Search API E2E Tests from Search Services (#4172)

This commit is contained in:
Mousumi Podder
2026-08-06 14:40:44 +05:30
committed by GitHub
parent bdc06d33b0
commit 5a7aad52ae
50 changed files with 7837 additions and 12 deletions
+4 -3
View File
@@ -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) }}
+14 -6
View File
@@ -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
}
],
@@ -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
+1 -1
View File
@@ -10,10 +10,10 @@
</parent>
<modules>
<module>tas-restapi</module>
<module>tas-cmis</module>
<module>tas-email</module>
<module>tas-webdav</module>
<module>tas-restapi</module>
<module>tas-integration</module>
</modules>
+13
View File
@@ -41,6 +41,12 @@
<suiteXmlFile>${project.basedir}/src/test/resources/test-suites/part3-suite.xml</suiteXmlFile>
</properties>
</profile>
<profile>
<id>elasticsearch-e2e-tests</id>
<properties>
<suiteXmlFile>${project.basedir}/src/test/resources/elasticsearch-e2e-suite.xml</suiteXmlFile>
</properties>
</profile>
</profiles>
<dependencies>
@@ -179,6 +185,13 @@
<groupId>org.eclipse.parsson</groupId>
<artifactId>parsson</artifactId>
</dependency>
<dependency>
<groupId>org.alfresco.tas</groupId>
<artifactId>cmis</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@@ -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
@@ -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 <http://www.gnu.org/licenses/>.
* #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.
* <p>
* 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<String> expectedNames, SearchLanguage queryLanguage)
{
SearchResponse response = performSearch(testUser, query, queryLanguage, getDefaultPagingOptions());
List<String> 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<String> 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<String> expectedNames, SearchLanguage queryLanguage)
{
SearchResponse response = performSearch(testUser, query, queryLanguage, getDefaultPagingOptions());
Set<String> 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('<uuid>')"})
* @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:
*
* <pre>
* {
* "query": { "query": "&lt;query&gt;", "userQuery": "&lt;userQuery&gt;" },
* "spellcheck": { "query": "&lt;userQuery&gt;" }
* }
* </pre>
*
* 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:
* <ul>
* <li>If {@code spellCheckType} is {@code null}, asserts the {@code spellCheck} field is null.</li>
* <li>Otherwise asserts {@code type} equals {@code spellCheckType}.</li>
* <li>If {@code spellCheckSuggestion} is non-null, asserts it appears in {@code suggestions}.</li>
* </ul>
*/
@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);
}
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<Map<String, String>> 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<FileModel> createdFileModels = new ArrayList<>();
range(0, exactSearchData.size()).forEach(id -> {
Map<String, String> record = exactSearchData.get(id);
Map<String, Object> 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.");
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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:
* <ul>
* <li>Preparing the data to index.</li>
* <li>Polling the search API until results are present.</li>
* </ul>
* <p>
* 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:
*
* <pre>
* |- FolderSearch
* |-- pangram.txt
* |-- cars.PDF
* |-- alfresco.docx
* |-- &lt;uniqueFileName&gt;.ODT
* </pre>
*/
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);
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<String, Object> 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");
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<RestRequestFacetFieldModel> 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<RestRequestFacetFieldModel> 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<RestResultBucketsModel> 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<RestRequestFacetFieldModel> 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<RestRequestFacetFieldModel> 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<RestResultBucketsModel> 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");
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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}");
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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 20112015). */
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<RestRequestRangesModel> 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<String, String> metric = (Map<String, String>) bucket.getMetrics().getFirst().getValue();
assertEquals(Integer.valueOf(metric.get("count")).intValue(), 2, "Unexpected count for first bucket.");
Map<String, String> info = (Map<String, String>) 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<String, String>) bucket.getMetrics().getFirst().getValue();
assertEquals(Integer.valueOf(metric.get("count")).intValue(), 1, "Unexpected count for second bucket.");
info = (Map<String, String>) 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<String, String>) 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<RestRequestRangesModel> 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<String, String> metric = (Map<String, String>) bucket.getMetrics().getFirst().getValue();
assertEquals(Integer.valueOf(metric.get("count")).intValue(), 2, "Unexpected count for first bucket.");
Map<String, String> info = (Map<String, String>) 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<String, String>) bucket.getBucketInfo();
assertEquals(info.get("start"), "40");
assertEquals(info.get("end"), "120");
metric = (Map<String, String>) 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<String, String>) bucket.getMetrics().getFirst().getValue();
assertEquals(Integer.valueOf(metric.get("count")).intValue(), 1, "Unexpected count for third bucket.");
info = (Map<String, String>) 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<RestRequestRangesModel> 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<RestGenericBucketModel> 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<String, String> info = (Map<String, String>) 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<RestRequestRangesModel> 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<String> include = List.of("upper");
facetRangeModel.setInclude(include);
List<RestRequestRangesModel> 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<String, String> metric = (Map<String, String>) bucket.getMetrics().getFirst().getValue();
assertEquals(Integer.valueOf(metric.get("count")).intValue(), 2, "Unexpected count for first bucket.");
Map<String, String> info = (Map<String, String>) 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<String, String>) bucket.getMetrics().getFirst().getValue();
assertEquals(Integer.valueOf(metric.get("count")).intValue(), 1, "Unexpected count for second bucket.");
info = (Map<String, String>) 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<String, String>) bucket.getMetrics().getFirst().getValue();
assertEquals(Integer.valueOf(metric.get("count")).intValue(), 1, "Unexpected count for third bucket.");
info = (Map<String, String>) 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}).
*
* <p>
* 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);
}
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<FacetQuery> 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<RestRequestFacetFieldModel> 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<FacetQuery> 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<RestRequestFacetFieldModel> 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<RestRequestFacetFieldModel> 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<RestRequestFacetFieldModel> 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<String, Object> 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<RestRequestFacetFieldModel> 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}]");
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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 <a href="https://issues.alfresco.com/jira/browse/SEARCH-2139">...</a>
*/
@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<String> mlMultipleValue = new ArrayList<String>();
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);
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<RestRequestFacetFieldModel> list = new ArrayList<>();
list.add(new RestRequestFacetFieldModel("'creator'"));
facetFields.setFacets(list);
query.setFacetFields(facetFields);
query.setIncludeRequest(false);
List<RestRequestPivotModel> 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<RestRequestFacetFieldModel> 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<RestRequestPivotModel> 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<RestRequestFacetFieldModel> 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<RestRequestPivotModel> 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<RestRequestFacetFieldModel> 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<RestRequestRangesModel> ranges = new ArrayList<>();
ranges.add(facetRangeModel);
query.setRanges(ranges);
List<RestRequestPivotModel> 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<RestRequestFacetFieldModel> 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<RestRequestPivotModel> 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="));
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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);
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<FacetFieldBucket> 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<FacetFieldBucket> 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<FacetFieldBucket> 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<FacetFieldBucket> getBuckets(SearchResponse response)
{
List<RestResultBucketsModel> facetFields = response.getContext().getFacetsFields();
Assert.assertNotNull(facetFields);
Assert.assertFalse(facetFields.isEmpty());
List<FacetFieldBucket> buckets = facetFields.iterator().next().getBuckets();
Assert.assertNotNull(buckets);
Assert.assertFalse(buckets.isEmpty());
return buckets;
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<String, Object> 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<String, String> foundProperties = (Map<String, String>) 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<Object> aspects = doc.getProperty(PropertyIds.SECONDARY_OBJECT_TYPE_IDS).getValues();
aspects.remove("P:finance:ParkEx");
Map<String, Object> 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<String, String>) 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");
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<RestRequestFacetFieldModel> 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));
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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\"");
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<RestRequestFieldsModel> 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<RestRequestFieldsModel> 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<RestRequestFieldsModel> 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());
}));
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<String, Object> 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");
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<GroupModel> 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() + ")");
}
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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);
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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");
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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");
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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();
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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.
*
* <pre>
* {
* "query": { "query": "cm:title:alfrezco", "language": "afts" },
* "spellcheck": { "query": "alfrezco" }
* }
* </pre>
*/
@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 <spellcheckspacebar> to testSite
FileModel file1 = new FileModel("spellcheckspacebar", "", "", FileType.TEXT_PLAIN, "spellcheckspacebar");
dataContent.usingUser(testUser).usingSite(testSite).createContent(file1);
// Add file <spellcheckspacecar> 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);
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<String> 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<String> 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<String> 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));
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<String, Object> 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);
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<String> stores = Arrays.asList("workspace://SpacesStore", "archive://SpacesStore");
List<String> baseUrls = Arrays.asList("/solr/alfresco", "/solr/archive");
List<RestShardInfoModel> entries = info.getEntries();
for (RestShardInfoModel shardInfoModel : entries)
{
RestShardInfoModel model = shardInfoModel.getModel();
assertEquals(model.getTemplate(), "rerank");
assertEquals(model.getMode(), "MASTER");
List<String> 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<RestShardModel> shards = model.getShards();
assertNotNull(shards);
RestShardModel shard = shards.iterator().next();
assertNotNull(shard);
List<RestInstanceModel> 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<String> stores = Arrays.asList("workspace://SpacesStore", "archive://SpacesStore");
List<String> baseUrls = Arrays.asList("/solr/alfresco", "/solr/archive");
List<RestShardInfoModel> 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<RestShardModel> shards = model.getShards();
assertNotNull(shards);
RestShardModel shard = shards.iterator().next();
assertNotNull(shard);
List<RestInstanceModel> 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<String> stores = Sets.newHashSet("workspace://SpacesStore", "archive://SpacesStore");
List<String> baseUrls = Arrays.asList("/solr/alfresco", "/solr/archive");
List<RestShardInfoModel> entries = info.getEntries();
Set<String> 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<String> 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<RestShardModel> shards = model.getShards();
assertNotNull(shards, "There are no shards present");
for (RestShardModel shardInstance : shards)
{
List<RestInstanceModel> 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());
}
}
}
}
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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));
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<RestRequestStatsModel> 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<String> 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<RestRequestStatsModel> 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<RestRequestStatsModel> 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<RestRequestFacetFieldModel> 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<RestRequestPivotModel> 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<RestRequestStatsModel> 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<RestRequestFacetFieldModel> 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<RestRequestPivotModel> 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<RestRequestStatsModel> 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<String> 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<RestGenericMetricModel> metrics = bucket.getMetrics();
assertEquals(metrics.size(), metricsCount);
return metrics.stream().map(RestGenericMetricModel::getType).collect(Collectors.toSet());
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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);
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<String> 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<String> 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<String> 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");
}
}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<String> secondaryTypes = List.of("P:shard:sharding");
Map<String, Object> 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<String, Object> 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<String, Object> 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);
}
}
@@ -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
@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="ElasticSearchE2ETests" verbose="1" preserve-order="true">
<listeners>
<listener class-name="org.alfresco.utility.report.HtmlReportListener"/>
<listener class-name="org.alfresco.utility.testrail.TestRailExecutorListener"/>
<listener class-name="org.alfresco.utility.testng.OSTestMethodSelector"/>
</listeners>
<test name="SearchTests">
<packages>
<package name="org.alfresco.rest.search.*">
<exclude name="org.alfresco.rest.search.fingerprint"/>
<exclude name="org.alfresco.rest.search.crosslocale"/>
</package>
</packages>
<classes>
<class name="org.alfresco.rest.search.SearchSpellCheckTest">
<methods>
<exclude name=".*"/>
</methods>
</class>
<class name="org.alfresco.rest.search.NodeContentTest">
<methods>
<exclude name=".*"/>
</methods>
</class>
<!-- 1) StatsSearchTest: not supported by ES -->
<class name="org.alfresco.rest.search.StatsSearchTest">
<methods>
<exclude name=".*"/>
</methods>
</class>
<!-- 2) ShardInfoTest: /private/search/versions/1/info depends on Solr-only shardRegistry bean.
Keep getShardInfoWithoutAdminAuthority() which only asserts 500 and passes on ES. -->
<class name="org.alfresco.rest.search.ShardInfoTest">
<methods>
<exclude name="getShardInfoWithAdminAuthority"/>
</methods>
</class>
<!-- 3) SearchNonIndexedFields.testNonIndexedDoesNotExist:uses Solr-only Luke admin endpoint /solr/alfresco/admin/luke -->
<class name="org.alfresco.rest.search.SearchNonIndexedFields">
<methods>
<exclude name="testNonIndexedDoesNotExist"/>
</methods>
</class>
<!-- 4) SearchHighLightTest.searchNonIndexedData:AFTS bare-property-name parsing gap on ES (returns HTTP 500) -->
<class name="org.alfresco.rest.search.SearchHighLightTest">
<methods>
<exclude name="searchNonIndexedData"/>
</methods>
</class>
<!-- 5) SearchAPATHTest: APATH is a Solr-only indexed field -->
<class name="org.alfresco.rest.search.SearchAPATHTest">
<methods>
<exclude name=".*"/>
</methods>
</class>
<!-- 6) SearchAFTSInFieldTest: ES tokenizer / cross-locale / ~ operator gaps -->
<class name="org.alfresco.rest.search.SearchAFTSInFieldTest">
<methods>
<exclude name="testSearchInFieldName"/>
<exclude name="testSearchInFieldNameQueryExpansion"/>
<exclude name="testExactMatchWithoutCrossLocale"/>
<exclude name="testWithConjunctionDisjunctionAndNegation"/>
</methods>
</class>
<!-- 7) PivotFacetedSearchTest: not supported by ES -->
<class name="org.alfresco.rest.search.PivotFacetedSearchTest">
<methods>
<exclude name=".*"/>
</methods>
</class>
<!-- 8) FieldDefinitionTest: unknown-property error handling & mltext stemming gaps -->
<class name="org.alfresco.rest.search.FieldDefinitionTest">
<methods>
<exclude name="testTextFieldNotDefined"/>
<exclude name="testmlTextFieldNotDefined"/>
<exclude name="testmlTextFieldMuliple"/>
</methods>
</class>
<!-- 9) FacetRangeSearchTest: range facet gaps on ES -->
<class name="org.alfresco.rest.search.FacetRangeSearchTest">
<methods>
<exclude name="searchWithRange"/>
<exclude name="searchWithRangeHardend"/>
<exclude name="searchWithRangeAndIncludeUpperBound"/>
</methods>
</class>
<!-- 10) FacetIntervalSearchTest: interval facet gaps on ES -->
<class name="org.alfresco.rest.search.FacetIntervalSearchTest">
<methods>
<exclude name="searchWithBasicInterval"/>
<exclude name="searchWithDates"/>
</methods>
</class>
<!-- 11) FacetFieldsSearchTest: mincount / SITE facet gaps on ES -->
<class name="org.alfresco.rest.search.FacetFieldsSearchTest">
<methods>
<exclude name="testSearchFacetFieldsBucketExcludedWhenMinCount2"/>
<exclude name="testSearchWithFacetFieldsMinCountChecks"/>
<exclude name="testSearchWithFacetFieldsOnlyFacetsWhereAccess"/>
</methods>
</class>
<!-- 12) FacetedSearchTest: not supported by ES -->
<class name="org.alfresco.rest.search.FacetedSearchTest">
<methods>
<exclude name=".*"/>
</methods>
</class>
<!-- 13) SearchExactTermTest: ES exact-term validation & cm:name over-match gaps -->
<class name="org.alfresco.rest.search.SearchExactTermTest">
<methods>
<exclude name="exactSearch_singleTermConjunction_shouldReturnException"/>
<exclude name="exactSearch_multiTermInFieldWithOnlyUnTokenizedAnalysis_shouldReturnFullFieldValueMatch"/> <!-- pragma: allowlist secret -->
<exclude name="exactSearch_singleTerm_shouldReturnResultsContainingExactTermInName"/>
<exclude name="exactSearch_multiTerm_shouldReturnResultsContainingExactTerm"/>
<exclude name="exactSearch_exactPhrase_shouldReturnResultsContainingExactPhrase"/>
</methods>
</class>
<!-- 14) SearchCasesTest:
- testSearchExactTermQueries: '=' exact-term operator is disabled by default on Search Enterprise; enable in follow-up PR -->
<class name="org.alfresco.rest.search.SearchCasesTest">
<methods>
<exclude name="testSearchExactTermQueries"/>
</methods>
</class>
</classes>
</test>
</suite>
@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8"?>
<model xmlns="http://www.alfresco.org/model/dictionary/1.0" name="finance:Finance">
<author>Administrator</author>
<imports>
<import uri="http://www.alfresco.org/model/content/1.0" prefix="cm"/>
<import uri="http://www.alfresco.org/model/dictionary/1.0" prefix="d"/>
</imports>
<namespaces>
<namespace uri="Finance" prefix="finance"/>
</namespaces>
<data-types/>
<constraints/>
<types>
<type name="finance:Receipt">
<title>Receipt</title>
<parent>cm:content</parent>
<properties>
<property name="finance:ReceiptValue">
<title>ReceiptValue</title>
<type>d:double</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>false</tokenised>
<facetable>true</facetable>
</index>
</property>
<property name="finance:ReceiptNo">
<title>ReceiptNo</title>
<type>d:long</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
</index>
</property>
</properties>
<associations/>
<overrides/>
<mandatory-aspects/>
</type>
<type name="finance:Expense">
<title>Expense</title>
<parent>cm:content</parent>
<properties>
<property name="finance:No">
<title>No</title>
<type>d:long</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
<facetable>true</facetable>
</index>
</property>
<property name="finance:Emp">
<title>Emp</title>
<type>d:text</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
<facetable>true</facetable>
</index>
</property>
<property name="finance:amount">
<title>Amount</title>
<type>d:double</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
<facetable>true</facetable>
</index>
</property>
<property name="finance:Title">
<title>Title</title>
<type>d:text</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>both</tokenised>
<facetable>false</facetable>
</index>
</property>
<property name="finance:Desc">
<title>Description</title>
<type>d:text</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>false</tokenised>
<facetable>false</facetable>
</index>
</property>
<property name="finance:CreatedAt">
<title>CreatedAt</title>
<type>d:datetime</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
</index>
</property>
<property name="finance:Location">
<title>Location</title>
<type>d:text</type>
<mandatory>true</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
<facetable>true</facetable>
</index>
</property>
<property name="finance:EmpNo">
<title>EmpNo</title>
<type>d:int</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
<facetable>true</facetable>
</index>
</property>
<property name="finance:EmpDept">
<title>EmpDept</title>
<type>d:float</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
<facetable>true</facetable>
</index>
</property>
</properties>
<associations/>
<overrides/>
<mandatory-aspects/>
</type>
</types>
<aspects>
<aspect name="finance:ParkEx">
<title>Parking Expense</title>
<properties>
<property name="finance:ParkingLocation">
<title>Location</title>
<type>d:text</type>
<mandatory>true</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
<facetable>true</facetable>
</index>
</property>
</properties>
<associations/>
<overrides/>
<mandatory-aspects/>
</aspect>
</aspects>
</model>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<model xmlns="http://www.alfresco.org/model/dictionary/1.0" name="index:IndexingDisabledContentModel">
<description>Indexing Disabled Content Model for Non-Indexed Fields E2E Tests</description>
<author>Alfresco</author>
<imports>
<import uri="http://www.alfresco.org/model/content/1.0" prefix="cm"/>
<import uri="http://www.alfresco.org/model/dictionary/1.0" prefix="d"/>
</imports>
<namespaces>
<namespace uri="http://www.alfresco.org/model/index/1.0" prefix="index"/>
</namespaces>
<types>
<type name="index:sample">
<title>Sample</title>
<parent>cm:content</parent>
<properties>
<property name="index:indexed">
<title>Indexed Field</title>
<type>d:text</type>
<mandatory>false</mandatory>
<index enabled="true"><tokenised>true</tokenised></index>
</property>
<property name="index:nonIndexed">
<title>Non-Indexed Field</title>
<type>d:text</type>
<mandatory>false</mandatory>
<index enabled="false"/>
</property>
</properties>
</type>
</types>
</model>
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<model xmlns="http://www.alfresco.org/model/dictionary/1.0" name="allfieldtypes:AllFieldTypesModel">
<description>All Field Types Model for Search Field Definition Tests (SEARCH-2139)</description>
<author>Alfresco</author>
<imports>
<import uri="http://www.alfresco.org/model/content/1.0" prefix="cm"/>
<import uri="http://www.alfresco.org/model/dictionary/1.0" prefix="d"/>
</imports>
<namespaces>
<namespace uri="http://www.alfresco.org/model/allfieldtypes/1.0" prefix="allfieldtypes"/>
</namespaces>
<aspects>
<aspect name="allfieldtypes:text">
<title>All Field Types Text Aspect</title>
<properties>
<property name="allfieldtypes:textLOVPartial">
<title>Text LOV Partial</title>
<type>d:text</type>
<mandatory>false</mandatory>
<index enabled="true"><tokenised>true</tokenised></index>
</property>
<property name="allfieldtypes:textPatternUnique">
<title>Text Pattern Unique</title>
<type>d:text</type>
<mandatory>false</mandatory>
<index enabled="true"><tokenised>true</tokenised></index>
</property>
<property name="allfieldtypes:textFree">
<title>Text Free</title>
<type>d:text</type>
<mandatory>false</mandatory>
<index enabled="true"><tokenised>true</tokenised></index>
</property>
<property name="allfieldtypes:textPatternMany">
<title>Text Pattern Many</title>
<type>d:text</type>
<mandatory>false</mandatory>
<index enabled="true"><tokenised>true</tokenised></index>
</property>
<property name="allfieldtypes:textLOVWhole">
<title>Text LOV Whole (not tokenised)</title>
<type>d:text</type>
<mandatory>false</mandatory>
<index enabled="true"><tokenised>false</tokenised></index>
</property>
<property name="allfieldtypes:mltextLOVPartial">
<title>MLText LOV Partial</title>
<type>d:mltext</type>
<mandatory>false</mandatory>
<index enabled="true"><tokenised>true</tokenised></index>
</property>
<property name="allfieldtypes:mltextFree">
<title>MLText Free</title>
<type>d:mltext</type>
<mandatory>false</mandatory>
<index enabled="true"><tokenised>true</tokenised></index>
</property>
<property name="allfieldtypes:mltextLOVWhole">
<title>MLText LOV Whole (not tokenised)</title>
<type>d:mltext</type>
<mandatory>false</mandatory>
<index enabled="true"><tokenised>false</tokenised></index>
</property>
<property name="allfieldtypes:multiplemltext">
<title>Multiple MLText</title>
<type>d:mltext</type>
<mandatory>false</mandatory>
<multiple>true</multiple>
<index enabled="true"><tokenised>true</tokenised></index>
</property>
</properties>
</aspect>
</aspects>
</model>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="SearchE2ECrossLocaleTests" verbose="1" preserve-order="true">
<listeners>
<listener class-name="org.alfresco.utility.report.HtmlReportListener"/>
<listener class-name="org.alfresco.utility.testrail.TestRailExecutorListener"/>
<listener class-name="org.alfresco.utility.testng.OSTestMethodSelector"/>
</listeners>
<test name="SearchE2ECrossLocaleTests">
<packages>
<package name="org.alfresco.rest.search.crosslocale"/>
</packages>
</test>
</suite>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<model xmlns="http://www.alfresco.org/model/dictionary/1.0" name="shard:ShardingContentModel">
<description>Sharding Content Model for Explicit Routing E2E Tests</description>
<author>Alfresco</author>
<imports>
<import uri="http://www.alfresco.org/model/content/1.0" prefix="cm"/>
<import uri="http://www.alfresco.org/model/dictionary/1.0" prefix="d"/>
</imports>
<namespaces>
<namespace uri="http://www.alfresco.org/model/shard/1.0" prefix="shard"/>
</namespaces>
<aspects>
<aspect name="shard:sharding">
<title>Sharding</title>
<properties>
<property name="shard:shardId">
<title>Shard ID</title>
<type>d:text</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>false</tokenised>
</index>
</property>
</properties>
</aspect>
</aspects>
</model>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="SolrSearchE2EFingerprintTests" verbose="1" preserve-order="true">
<listeners>
<listener class-name="org.alfresco.utility.report.HtmlReportListener"/>
<listener class-name="org.alfresco.utility.testrail.TestRailExecutorListener"/>
<listener class-name="org.alfresco.utility.testng.OSTestMethodSelector"/>
</listeners>
<test name="SolrSearchE2EFingerprintTests">
<packages>
<package name="org.alfresco.rest.search.fingerPrint"/>
</packages>
</test>
</suite>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="SolrSearchE2ESpellcheckTests" verbose="1" preserve-order="true">
<listeners>
<listener class-name="org.alfresco.utility.report.HtmlReportListener"/>
<listener class-name="org.alfresco.utility.testrail.TestRailExecutorListener"/>
<listener class-name="org.alfresco.utility.testng.OSTestMethodSelector"/>
</listeners>
<test name="SolrSearchE2ESpellcheckTests">
<classes>
<class name="org.alfresco.rest.search.SearchSpellCheckTest"/>
</classes>
</test>
</suite>
@@ -15,7 +15,15 @@
<package name="org.alfresco.rest.people.*"/>
<package name="org.alfresco.rest.queries.*"/>
<package name="org.alfresco.rest.ratings.*"/>
<package name="org.alfresco.rest.search.*"/>
</packages>
<!--
Including the full "org.alfresco.rest.search.*" package pushes Part2
over the 60-min CI limit after the recent test migration. Those tests
already run in a dedicated CI profile, so only NodeContentTest is
kept here to preserve the original Part2 coverage. -->
<classes>
<class name="org.alfresco.rest.search.NodeContentTest"/>
</classes>
</test>
</suite>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<model xmlns="http://www.alfresco.org/model/dictionary/1.0" name="tok:tok">
<imports>
<import uri="http://www.alfresco.org/model/content/1.0" prefix="cm"/>
<import uri="http://www.alfresco.org/model/dictionary/1.0" prefix="d"/>
</imports>
<namespaces>
<namespace uri="http://www.alfresco.org/model/tokenised/1.0" prefix="tok"/>
</namespaces>
<types>
<type name="tok:document">
<parent>cm:content</parent>
<properties>
<property name="tok:true">
<type>d:text</type>
<index enabled="true">
<tokenised>true</tokenised>
</index>
</property>
<property name="tok:false">
<type>d:text</type>
<index enabled="true">
<tokenised>false</tokenised>
</index>
</property>
<property name="tok:both">
<type>d:text</type>
<index enabled="true">
<tokenised>both</tokenised>
</index>
</property>
</properties>
</type>
</types>
<aspects/>
</model>
@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8"?>
<model xmlns="http://www.alfresco.org/model/dictionary/1.0" name="finance:Finance">
<author>Administrator</author>
<imports>
<import uri="http://www.alfresco.org/model/content/1.0" prefix="cm"/>
<import uri="http://www.alfresco.org/model/dictionary/1.0" prefix="d"/>
</imports>
<namespaces>
<namespace uri="Finance" prefix="finance"/>
</namespaces>
<data-types/>
<constraints/>
<types>
<type name="finance:Receipt">
<title>Receipt</title>
<parent>cm:content</parent>
<properties>
<property name="finance:ReceiptValue">
<title>ReceiptValue</title>
<type>d:double</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>false</tokenised>
<facetable>true</facetable>
</index>
</property>
<property name="finance:ReceiptNo">
<title>ReceiptNo</title>
<type>d:long</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
</index>
</property>
</properties>
<associations/>
<overrides/>
<mandatory-aspects/>
</type>
<type name="finance:Expense">
<title>Expense</title>
<parent>cm:content</parent>
<properties>
<property name="finance:No">
<title>No</title>
<type>d:long</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
<facetable>true</facetable>
</index>
</property>
<property name="finance:Emp">
<title>Emp</title>
<type>d:text</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
<facetable>true</facetable>
</index>
</property>
<property name="finance:amount">
<title>Amount</title>
<type>d:double</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
<facetable>true</facetable>
</index>
</property>
<property name="finance:Title">
<title>Title</title>
<type>d:text</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>both</tokenised>
<facetable>false</facetable>
</index>
</property>
<property name="finance:Desc">
<title>Description</title>
<type>d:text</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>false</tokenised>
<facetable>false</facetable>
</index>
</property>
<property name="finance:CreatedAt">
<title>CreatedAt</title>
<type>d:datetime</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
</index>
</property>
<property name="finance:Location">
<title>Location</title>
<type>d:text</type>
<mandatory>true</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
<facetable>true</facetable>
</index>
</property>
<property name="finance:EmpNo">
<title>EmpNo</title>
<type>d:int</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
<facetable>true</facetable>
</index>
</property>
<property name="finance:EmpDept">
<title>EmpDept</title>
<type>d:float</type>
<mandatory>false</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
<facetable>true</facetable>
</index>
</property>
</properties>
<associations/>
<overrides/>
<mandatory-aspects/>
</type>
</types>
<aspects>
<aspect name="finance:ParkEx">
<title>Parking Expense</title>
<properties>
<property name="finance:ParkingLocation">
<title>Location</title>
<type>d:text</type>
<mandatory>true</mandatory>
<index enabled="true">
<tokenised>true</tokenised>
<facetable>true</facetable>
</index>
</property>
</properties>
<associations/>
<overrides/>
<mandatory-aspects/>
</aspect>
</aspects>
</model>
+4
View File
@@ -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