diff --git a/packaging/tests/tas-restapi/pom.xml b/packaging/tests/tas-restapi/pom.xml
index 34298a670a..63215b2a96 100644
--- a/packaging/tests/tas-restapi/pom.xml
+++ b/packaging/tests/tas-restapi/pom.xml
@@ -8,7 +8,7 @@
org.alfresco
alfresco-community-repo-tests
- 26.3.0.36-SNAPSHOT
+ 26.3.0.50-SNAPSHOT
@@ -42,9 +42,21 @@
- elasticsearch-e2e-tests
+ elasticsearch-e2e-tests-part1
- ${project.basedir}/src/test/resources/elasticsearch-e2e-suite.xml
+ ${project.basedir}/src/test/resources/test-suites/elasticsearch-e2e-part1-suite.xml
+
+
+
+ elasticsearch-e2e-tests-part2
+
+ ${project.basedir}/src/test/resources/test-suites/elasticsearch-e2e-part2-suite.xml
+
+
+
+ elasticsearch-e2e-tests-part3
+
+ ${project.basedir}/src/test/resources/test-suites/elasticsearch-e2e-part3-suite.xml
diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/DocumentLibraryTagFilterTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/DocumentLibraryTagFilterTest.java
index 3d2ee1d577..cbbe9cf4c7 100644
--- a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/DocumentLibraryTagFilterTest.java
+++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/DocumentLibraryTagFilterTest.java
@@ -28,60 +28,54 @@ package org.alfresco.rest.search;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import java.util.List;
+import java.util.stream.Collectors;
-import io.restassured.path.json.JsonPath;
-import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
-import org.alfresco.rest.core.RestRequest;
-import org.alfresco.utility.Utility;
+import org.alfresco.rest.model.RestTagModel;
import org.alfresco.utility.data.RandomData;
import org.alfresco.utility.model.FileModel;
import org.alfresco.utility.model.FileType;
/**
- * End-to-end test for the Share Document Library "tag" filter (slingshot {@code doclist} webscript, driven by {@code filters.lib.js}) running against a real search server.
+ * End-to-end test for tag-based document filtering, verified through the public Search REST API ({@code /alfresco/api/-default-/public/search/versions/1/search}) running against a real search server.
*
- * clicking a tag that contains a space used to return either no documents or every document. This test tags documents through the public v1 REST API, waits for the live index to catch up, then calls the same {@code /slingshot/doclib2/doclist} endpoint the Share UI uses and asserts that the tag filter returns exactly the tagged document - for both a single-word tag and a tag containing a space.
+ * Clicking a tag that contains a space used to return either no documents or every document. This test tags documents through the public v1 REST API (capturing each tag's category nodeRef from the response), waits for the live index to catch up, then runs an exact {@code +=cm\:taggable:""} membership query and asserts that the tag filter returns exactly the tagged document - for both a single-word tag and a tag containing a space. The exact-term match on the tag nodeRef is honoured by both Solr and Elasticsearch, mirroring the Share {@code filters.lib.js} tag filter.
*
- * The test lives in {@code org.alfresco.rest.search} so it is picked up automatically by the Elasticsearch E2E suite ({@code elasticsearch-e2e-suite.xml}), proving the fix works against an Elasticsearch server.
+ * The test lives in {@code org.alfresco.rest.search} so it is picked up automatically by the Elasticsearch E2E suite ({@code elasticsearch-e2e-suite.xml}), proving the behaviour works against an Elasticsearch server. Using the Search API (rather than the Share {@code slingshot/doclib2/doclist} webscript) keeps the test runnable on the community-repo stack, which does not deploy the share-services module.
*/
@SuppressWarnings({"PMD.MethodNamingConventions", "PMD.LongVariable"})
public class DocumentLibraryTagFilterTest extends AbstractE2EFunctionalTest
{
- /** Webscript service prefix for the slingshot doclist endpoint (equivalent to {@code /alfresco/s}). */
- private static final String DOCLIST_BASE_PATH = "alfresco/service/slingshot/doclib2/doclist";
-
- /** Default Share Document Library container name. */
- private static final String DOCUMENT_LIBRARY = "documentLibrary";
-
- private String singleWordTag;
- private String spaceTag;
-
private FileModel singleWordTaggedFile;
private FileModel spaceTaggedFile;
+ private String singleWordTagNodeRef;
+ private String spaceTagNodeRef;
+
@BeforeClass(alwaysRun = true)
public void dataPreparation()
{
// Unique suffix keeps the tags private to this test run (the tag filter is repo-wide, not site-scoped).
String unique = RandomData.getRandomName("Tag").toLowerCase();
- singleWordTag = "single" + unique;
- spaceTag = "long " + unique; // contains a space - the scenario that used to fail
+ String singleWordTag = "single" + unique;
+ String spaceTag = "long " + unique; // contains a space - the scenario that used to fail
- singleWordTaggedFile = createTaggedFile(singleWordTag);
- spaceTaggedFile = createTaggedFile(spaceTag);
+ singleWordTaggedFile = createFile();
+ singleWordTagNodeRef = tagFileAndGetTagNodeRef(singleWordTaggedFile, singleWordTag);
- // Wait until both tags resolve through the doclist endpoint (category node + cm:taggable both indexed).
- assertTrue(waitForTagFilter(singleWordTag, singleWordTaggedFile.getName()),
+ spaceTaggedFile = createFile();
+ spaceTagNodeRef = tagFileAndGetTagNodeRef(spaceTaggedFile, spaceTag);
+
+ // Wait until both tags resolve through the Search API (cm:taggable indexed for each document).
+ assertTrue(waitForTagFilter(singleWordTagNodeRef, singleWordTaggedFile.getName()),
"Single-word tag was not indexed/searchable in time: " + singleWordTag);
- assertTrue(waitForTagFilter(spaceTag, spaceTaggedFile.getName()),
+ assertTrue(waitForTagFilter(spaceTagNodeRef, spaceTaggedFile.getName()),
"Space-containing tag was not indexed/searchable in time: " + spaceTag);
}
@@ -89,78 +83,78 @@ public class DocumentLibraryTagFilterTest extends AbstractE2EFunctionalTest
@Test
public void tagFilterWithSpaceInTagNameReturnsOnlyTheTaggedDocument()
{
- assertTagFilterReturnsExactly(spaceTag, spaceTaggedFile.getName(), singleWordTaggedFile.getName());
+ assertTagFilterReturnsExactly(spaceTagNodeRef, spaceTaggedFile.getName(), singleWordTaggedFile.getName());
}
/** Regression guard: single-word tags keep working exactly as before. */
@Test
public void tagFilterWithSingleWordTagReturnsOnlyTheTaggedDocument()
{
- assertTagFilterReturnsExactly(singleWordTag, singleWordTaggedFile.getName(), spaceTaggedFile.getName());
+ assertTagFilterReturnsExactly(singleWordTagNodeRef, singleWordTaggedFile.getName(), spaceTaggedFile.getName());
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
- /** Creates a text document in the test site's document library and tags it via the public v1 REST API. */
- private FileModel createTaggedFile(String tag)
+ /** Creates a text document in the test site's document library. */
+ private FileModel createFile()
{
FileModel file = FileModel.getRandomFileModel(FileType.TEXT_PLAIN, "MNT-25799 tag filter test content");
dataContent.usingUser(testUser).usingSite(testSite).createContent(file);
-
- restClient.authenticateUser(testUser).withCoreAPI().usingResource(file).addTag(tag);
- restClient.assertStatusCodeIs(HttpStatus.CREATED);
return file;
}
- /** Runs the tag filter and asserts it returns exactly the expected file and never the other (unrelated) file. */
- private void assertTagFilterReturnsExactly(String tag, String expectedFileName, String excludedFileName)
+ /** Tags the file via the public v1 REST API and returns the created tag category nodeRef. */
+ private String tagFileAndGetTagNodeRef(FileModel file, String tag)
{
- JsonPath json = tagFilter(tag);
- restClient.assertStatusCodeIs(HttpStatus.OK);
-
- List fileNames = json.getList("items.location.file");
- assertNotNull(fileNames, "Doclist response did not contain an items list for tag: " + tag);
- assertTrue(fileNames.contains(expectedFileName),
- "Tag filter '" + tag + "' did not return the tagged document '" + expectedFileName + "'. Got: " + fileNames);
- assertFalse(fileNames.contains(excludedFileName),
- "Tag filter '" + tag + "' incorrectly returned an unrelated document '" + excludedFileName + "'. Got: " + fileNames);
- assertEquals(json.getInt("totalRecords"), 1,
- "Tag filter '" + tag + "' returned an unexpected number of documents. Got: " + fileNames);
+ RestTagModel tagModel = restClient.authenticateUser(testUser).withCoreAPI().usingResource(file).addTag(tag);
+ restClient.assertStatusCodeIs(HttpStatus.CREATED);
+ return "workspace://SpacesStore/" + tagModel.getId();
}
- /** Polls the doclist tag filter until {@code expectedFileName} appears or the retry budget is exhausted. */
- private boolean waitForTagFilter(String tag, String expectedFileName)
+ /** Runs the tag filter and asserts it returns exactly the expected file and never the other (unrelated) file. */
+ private void assertTagFilterReturnsExactly(String tagNodeRef, String expectedFileName, String excludedFileName)
{
- for (int attempt = 0; attempt < SEARCH_MAX_ATTEMPTS; attempt++)
- {
- JsonPath json = tagFilter(tag);
- if (String.valueOf(HttpStatus.OK.value()).equals(restClient.getStatusCode()))
- {
- List fileNames = json.getList("items.location.file");
- if (fileNames != null && fileNames.contains(expectedFileName))
- {
- return true;
- }
- }
- Utility.waitToLoopTime(properties.getSolrWaitTimeInSeconds(),
- "Waiting for tag to be indexed. Attempt: " + (attempt + 1));
- }
- return false;
+ SearchResponse response = tagFilter(tagNodeRef);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+
+ List fileNames = resultFileNames(response);
+ assertTrue(fileNames.contains(expectedFileName),
+ "Tag filter '" + tagNodeRef + "' did not return the tagged document '" + expectedFileName + "'. Got: " + fileNames);
+ assertFalse(fileNames.contains(excludedFileName),
+ "Tag filter '" + tagNodeRef + "' incorrectly returned an unrelated document '" + excludedFileName + "'. Got: " + fileNames);
+ assertEquals(fileNames.size(), 1,
+ "Tag filter '" + tagNodeRef + "' returned an unexpected number of documents. Got: " + fileNames);
+ }
+
+ /** Polls the Search API tag filter until {@code expectedFileName} appears or the retry budget is exhausted. */
+ private boolean waitForTagFilter(String tagNodeRef, String expectedFileName)
+ {
+ return isContentInSearchResults(tagQuery(tagNodeRef), expectedFileName, true);
+ }
+
+ /** Runs the exact {@code +=cm\:taggable} membership search for the given tag nodeRef as {@link #testUser}. */
+ private SearchResponse tagFilter(String tagNodeRef)
+ {
+ return query(createQuery(tagQuery(tagNodeRef)));
}
/**
- * Calls the slingshot doclist webscript with the tag filter, as the Share UI does: {@code GET /alfresco/s/slingshot/doclib2/doclist/all/site/{site}/documentLibrary?filter=tag&filterData=}.
+ * Builds the AFTS query that matches documents carrying the given tag.
+ *
+ * MNT-25799: uses an exact ({@code =}) membership match on the tag category nodeRef, mirroring the Share {@code filters.lib.js} tag filter. This form is honoured by both Solr and Elasticsearch and returns exactly the tagged document(s) - including for tags containing spaces, since the match is on the nodeRef rather than a tokenised tag phrase.
*/
- private JsonPath tagFilter(String tag)
+ private String tagQuery(String tagNodeRef)
{
- restClient.authenticateUser(testUser);
- restClient.configureRequestSpec().setBasePath(DOCLIST_BASE_PATH);
+ return "+=cm\\:taggable:\"" + tagNodeRef + "\"";
+ }
- RestRequest request = RestRequest.simpleRequest(HttpMethod.GET,
- "all/site/{site}/{container}?filter=tag&filterData={filterData}",
- testSite.getId(), DOCUMENT_LIBRARY, tag);
- return restClient.process(request).getResponse().jsonPath();
+ /** Extracts the {@code cm:name} of every document returned by a search response. */
+ private List resultFileNames(SearchResponse response)
+ {
+ return response.getEntries().stream()
+ .map(entry -> entry.getModel().getName())
+ .collect(Collectors.toList());
}
}
diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchAdvancedQueryOperatorsTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchAdvancedQueryOperatorsTest.java
new file mode 100644
index 0000000000..11911fb9d0
--- /dev/null
+++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchAdvancedQueryOperatorsTest.java
@@ -0,0 +1,109 @@
+/*
+ * #%L
+ * Alfresco Search Services E2E Test
+ * %%
+ * Copyright (C) 2005 - 2026 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+
+package org.alfresco.rest.search;
+
+import org.springframework.http.HttpStatus;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.alfresco.utility.model.FileModel;
+import org.alfresco.utility.model.FolderModel;
+
+/**
+ * Migration test class for advanced AFTS query operators on Elasticsearch.
+ */
+public class SearchAdvancedQueryOperatorsTest extends AbstractSearchServicesE2ETest
+{
+ private FileModel fileWithoutTitle;
+ private FileModel proximityFile;
+
+ private static final String UNIQUE_PREFIX = "acsadvops";
+
+ // Unique fake tokens — avoid tokenization surprises and cross-run pollution.
+ private static final String TOKEN_A = "acsadvproxaaa";
+ private static final String TOKEN_B = "acsadvproxbbb";
+ private static final String TOKEN_C = "acsadvproxccc";
+ private static final String TOKEN_D = "acsadvproxddd";
+
+ @BeforeClass(alwaysRun = true)
+ public void dataPreparation()
+ {
+ FolderModel folder = dataContent.usingUser(testUser).usingSite(testSite)
+ .createFolderCmisApi(UNIQUE_PREFIX + "-folder");
+
+ // File created without setting cm:title — used for ISUNSET.
+ fileWithoutTitle = new FileModel(UNIQUE_PREFIX + "-file-without-title.txt");
+ fileWithoutTitle.setContent("Body of the file without a title");
+ dataContent.usingUser(testUser).usingResource(folder).createContent(fileWithoutTitle);
+
+ // Content: 4 unique tokens at known positions A=0, B=1, C=2, D=3.
+ // Between A and D there are exactly 2 words (B, C), so proximity *(2) must match.
+ proximityFile = new FileModel(UNIQUE_PREFIX + "-proximity-file.txt");
+ proximityFile.setContent(TOKEN_A + " " + TOKEN_B + " " + TOKEN_C + " " + TOKEN_D);
+ dataContent.usingUser(testUser).usingResource(folder).createContent(proximityFile);
+
+ waitForMetadataIndexing(fileWithoutTitle.getName(), true);
+ waitForMetadataIndexing(proximityFile.getName(), true);
+
+ // Poll until the file's content is truly indexed on ES.
+ Assert.assertTrue(isContentInSearchResults("cm:content:'" + TOKEN_A + "' AND cm:name:'" + proximityFile.getName() + "'",
+ proximityFile.getName(), true), "Setup: proximity file's content should be indexed on ES before running tests");
+ }
+
+ /**
+ * ISUNSET:"cm:title" must return the file whose cm:title was never set.
+ */
+ @Test(priority = 1)
+ public void testIsUnsetOperator()
+ {
+ String query = "ISUNSET:\"cm:title\" AND cm:name:'" + fileWithoutTitle.getName() + "'";
+ SearchResponse response = queryAsUser(testUser, query);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(response.getPagination().getCount() >= 1,
+ "Expected file without a title to be findable via ISUNSET on cm:title");
+ Assert.assertTrue(isContentInSearchResponse(response, fileWithoutTitle.getName()),
+ "Expected " + fileWithoutTitle.getName() + " in the ISUNSET results");
+ }
+
+ /**
+ * AFTS proximity syntax: 'wordA *(N) wordB' matches when at most N words separate the two.TOKEN_A (position 0) and TOKEN_D (position 3) have exactly 2 words between them (TOKEN_B, TOKEN_C), so *(2) must match.
+ */
+ @Test(priority = 2)
+ public void testProximitySearchUsingAftsSyntax()
+ {
+ String query = "cm:content:(" + TOKEN_A + " *(2) " + TOKEN_D + ") AND cm:name:'" +
+ proximityFile.getName() + "'";
+ SearchResponse response = queryAsUser(testUser, query);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(response.getPagination().getCount() >= 1,
+ "Expected AFTS proximity '" + TOKEN_A + " *(2) " + TOKEN_D +
+ "' to find the file (2 words between A and D in the content)");
+ Assert.assertTrue(isContentInSearchResponse(response, proximityFile.getName()),
+ "Expected " + proximityFile.getName() + " in the proximity results");
+ }
+}
diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchCategoriesTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchCategoriesTest.java
new file mode 100644
index 0000000000..11a2acb160
--- /dev/null
+++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchCategoriesTest.java
@@ -0,0 +1,320 @@
+/*
+ * #%L
+ * Alfresco Search Services E2E Test
+ * %%
+ * Copyright (C) 2005 - 2026 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+
+package org.alfresco.rest.search;
+
+import jakarta.json.Json;
+import jakarta.json.JsonObject;
+
+import org.springframework.http.HttpStatus;
+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.FolderModel;
+
+/**
+ * Migration test class for category-based search scenarios on Elasticsearch.
+ */
+public class SearchCategoriesTest extends AbstractSearchServicesE2ETest
+{
+ private FolderModel folder;
+ private FileModel fileWithPrimaryCategory;
+ private FileModel anotherFileWithPrimaryCategory;
+ private FileModel fileWithCategoryAndTag;
+ private FileModel fileWithBothCategories;
+ private FileModel fileForCategoryRemoval;
+ private FileModel fileForLateAssignment;
+ private FileModel isolationFileA;
+ private FileModel isolationFileB;
+
+ private String primaryCategoryNodeRef;
+ private String secondaryCategoryNodeRef;
+
+ private static final String COMBINED_TAG = "acsmigrationcombinedtag";
+
+ @BeforeClass(alwaysRun = true)
+ public void dataPreparation()
+ {
+ folder = dataContent.usingUser(testUser).usingSite(testSite).createFolderCmisApi("categories-folder");
+
+ primaryCategoryNodeRef = lookupCategoryRefByName("Software Document Classification");
+ secondaryCategoryNodeRef = lookupCategoryRefByName("Regions");
+
+ if (primaryCategoryNodeRef == null || secondaryCategoryNodeRef == null)
+ {
+ SearchResponse anyCats = queryAsUser(dataUser.getAdminUser(), "TYPE:'cm:category'");
+ Assert.assertTrue(anyCats.getPagination().getCount() >= 2,
+ "Test setup requires at least two cm:category nodes in the repository");
+ if (primaryCategoryNodeRef == null)
+ {
+ primaryCategoryNodeRef = "workspace://SpacesStore/" + anyCats.getEntries().get(0).getModel().getId();
+ }
+ if (secondaryCategoryNodeRef == null)
+ {
+ secondaryCategoryNodeRef = "workspace://SpacesStore/" + anyCats.getEntries().get(1).getModel().getId();
+ }
+ }
+
+ fileWithPrimaryCategory = createFileClassifiedInto("file-with-primary-category.txt",
+ "File classified into primary category", primaryCategoryNodeRef);
+
+ anotherFileWithPrimaryCategory = createFileClassifiedInto("another-file-with-primary-category.txt",
+ "Second file classified into primary category", primaryCategoryNodeRef);
+
+ fileWithCategoryAndTag = createFileClassifiedInto("file-with-category-and-tag.txt",
+ "File with both a category and a tag", primaryCategoryNodeRef);
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(fileWithCategoryAndTag).addTag(COMBINED_TAG);
+
+ fileWithBothCategories = createFileClassifiedInto("file-with-both-categories.txt",
+ "File classified into two categories", primaryCategoryNodeRef, secondaryCategoryNodeRef);
+
+ fileForCategoryRemoval = createFileClassifiedInto("file-for-category-removal.txt",
+ "File whose category will be removed to verify de-indexing", primaryCategoryNodeRef);
+
+ fileForLateAssignment = new FileModel("file-for-late-category-assignment.txt");
+ fileForLateAssignment.setContent("File that receives a category after creation");
+ dataContent.usingUser(testUser).usingResource(folder).createContent(fileForLateAssignment);
+
+ isolationFileA = createFileClassifiedInto("isolation-file-a.txt",
+ "First isolation test file (category will be removed)", secondaryCategoryNodeRef);
+ isolationFileB = createFileClassifiedInto("isolation-file-b.txt",
+ "Second isolation test file (category must stay)", secondaryCategoryNodeRef);
+
+ waitForMetadataIndexing(fileWithPrimaryCategory.getName(), true);
+ waitForMetadataIndexing(anotherFileWithPrimaryCategory.getName(), true);
+ waitForMetadataIndexing(fileWithCategoryAndTag.getName(), true);
+ waitForMetadataIndexing(fileWithBothCategories.getName(), true);
+ waitForMetadataIndexing(fileForCategoryRemoval.getName(), true);
+ waitForMetadataIndexing(fileForLateAssignment.getName(), true);
+ waitForMetadataIndexing(isolationFileA.getName(), true);
+ waitForMetadataIndexing(isolationFileB.getName(), true);
+
+ Assert.assertTrue(isContentInSearchResults("cm:categories:\"" + primaryCategoryNodeRef + "\"", fileWithPrimaryCategory.getName(), true),
+ "Setup: primary-category association should be searchable after batch-indexing catches up");
+ Assert.assertTrue(isContentInSearchResults("cm:categories:\"" + secondaryCategoryNodeRef + "\"", isolationFileA.getName(), true),
+ "Setup: secondary-category association should be searchable after batch-indexing catches up");
+ Assert.assertTrue(isContentInSearchResults("TAG:'" + COMBINED_TAG + "'", fileWithCategoryAndTag.getName(), true),
+ "Setup: combined tag should be searchable after batch-indexing catches up");
+ }
+
+ private String lookupCategoryRefByName(String name)
+ {
+ SearchResponse response = queryAsUser(dataUser.getAdminUser(),
+ "TYPE:'cm:category' AND cm:name:'" + name + "'");
+ if (response.getPagination().getCount() >= 1)
+ {
+ return "workspace://SpacesStore/" + response.getEntries().getFirst().getModel().getId();
+ }
+ return null;
+ }
+
+ private FileModel createFileClassifiedInto(String name, String content, String... categoryRefs)
+ {
+ FileModel file = new FileModel(name);
+ file.setContent(content);
+ dataContent.usingUser(testUser).usingResource(folder).createContent(file);
+
+ jakarta.json.JsonArrayBuilder catArray = Json.createArrayBuilder();
+ for (String ref : categoryRefs)
+ {
+ catArray.add(ref);
+ }
+
+ JsonObject body = Json.createObjectBuilder()
+ .add("aspectNames", Json.createArrayBuilder().add("cm:generalclassifiable"))
+ .add("properties", Json.createObjectBuilder().add("cm:categories", catArray))
+ .build();
+ restClient.authenticateUser(testUser).withCoreAPI().usingNode(file).updateNode(body.toString());
+ return file;
+ }
+
+ @Test(priority = 1)
+ public void testSearchByGeneralClassifiableAspect()
+ {
+ String query = "ASPECT:'cm:generalclassifiable' AND cm:name:'" + fileWithPrimaryCategory.getName() + "'";
+ SearchResponse response = queryAsUser(testUser, query);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(response.getPagination().getCount() >= 1,
+ "Expected file with the cm:generalclassifiable aspect to be findable");
+ Assert.assertTrue(isContentInSearchResponse(response, fileWithPrimaryCategory.getName()),
+ "Expected " + fileWithPrimaryCategory.getName() + " in the aspect query results");
+ }
+
+ @Test(priority = 2)
+ public void testSearchByCategoryProperty()
+ {
+ String query = "cm:categories:\"" + primaryCategoryNodeRef + "\" AND cm:name:'" +
+ fileWithPrimaryCategory.getName() + "'";
+ SearchResponse response = queryAsUser(testUser, query);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(response.getPagination().getCount() >= 1,
+ "Expected file classified into the category to be findable via cm:categories property");
+ Assert.assertTrue(isContentInSearchResponse(response, fileWithPrimaryCategory.getName()),
+ "Expected " + fileWithPrimaryCategory.getName() + " in the cm:categories query results");
+ }
+
+ @Test(priority = 3)
+ public void testCategoryAndTagCombined()
+ {
+ String query = "ASPECT:'cm:generalclassifiable' AND TAG:'" + COMBINED_TAG +
+ "' AND cm:name:'" + fileWithCategoryAndTag.getName() + "'";
+ SearchResponse response = queryAsUser(testUser, query);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(response.getPagination().getCount() >= 1,
+ "Expected file with both a category and the combined tag to be findable via combined query");
+ Assert.assertTrue(isContentInSearchResponse(response, fileWithCategoryAndTag.getName()),
+ "Expected " + fileWithCategoryAndTag.getName() + " in the category+tag query results");
+ }
+
+ @Test(priority = 4)
+ public void testMultipleFilesInSameCategory()
+ {
+ String pathClause = "PATH:\"/app:company_home/st:sites/cm:" + testSite.getTitle() + "/cm:documentLibrary//*\"";
+ String query = "cm:categories:\"" + primaryCategoryNodeRef + "\" AND " + pathClause;
+ SearchResponse response = queryAsUser(testUser, query);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(response.getPagination().getCount() >= 2,
+ "Expected at least two files in this site classified into the primary category");
+ Assert.assertTrue(isContentInSearchResponse(response, fileWithPrimaryCategory.getName()),
+ "Expected " + fileWithPrimaryCategory.getName() + " among the primary-category files");
+ Assert.assertTrue(isContentInSearchResponse(response, anotherFileWithPrimaryCategory.getName()),
+ "Expected " + anotherFileWithPrimaryCategory.getName() + " among the primary-category files");
+ }
+
+ @Test(priority = 5)
+ public void testFileInMultipleCategoriesFoundByEach()
+ {
+ String queryPrimary = "cm:categories:\"" + primaryCategoryNodeRef + "\" AND cm:name:'" +
+ fileWithBothCategories.getName() + "'";
+ SearchResponse fromPrimary = queryAsUser(testUser, queryPrimary);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(fromPrimary.getPagination().getCount() >= 1,
+ "Expected multi-category file to be findable via the primary category");
+ Assert.assertTrue(isContentInSearchResponse(fromPrimary, fileWithBothCategories.getName()),
+ "Expected " + fileWithBothCategories.getName() + " when querying the primary category");
+
+ String querySecondary = "cm:categories:\"" + secondaryCategoryNodeRef + "\" AND cm:name:'" +
+ fileWithBothCategories.getName() + "'";
+ SearchResponse fromSecondary = queryAsUser(testUser, querySecondary);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(fromSecondary.getPagination().getCount() >= 1,
+ "Expected multi-category file to be findable via the secondary category");
+ Assert.assertTrue(isContentInSearchResponse(fromSecondary, fileWithBothCategories.getName()),
+ "Expected " + fileWithBothCategories.getName() + " when querying the secondary category");
+ }
+
+ @Test(priority = 6)
+ public void testCategoryRemovalUpdatesIndex()
+ {
+ String queryBefore = "cm:categories:\"" + primaryCategoryNodeRef + "\" AND cm:name:'" +
+ fileForCategoryRemoval.getName() + "'";
+ SearchResponse before = queryAsUser(testUser, queryBefore);
+ Assert.assertTrue(before.getPagination().getCount() >= 1,
+ "File should be findable via its category before removal");
+ Assert.assertTrue(isContentInSearchResponse(before, fileForCategoryRemoval.getName()),
+ "Expected " + fileForCategoryRemoval.getName() + " to be present before category removal");
+
+ JsonObject body = Json.createObjectBuilder()
+ .add("properties", Json.createObjectBuilder()
+ .add("cm:categories", Json.createArrayBuilder()))
+ .build();
+ restClient.authenticateUser(testUser).withCoreAPI().usingNode(fileForCategoryRemoval).updateNode(body.toString());
+
+ Assert.assertTrue(isContentInSearchResults(queryBefore, fileForCategoryRemoval.getName(), false),
+ "File should NOT be findable via its category after removal");
+ }
+
+ @Test(priority = 7)
+ public void testAspectRestrictsSearchToClassifiedFiles()
+ {
+ // fileForLateAssignment has no aspect yet. The cm:name query tokens ('late', 'assignment')
+ // don't appear in any classified file, so a match here would indicate an ES over-match bug.
+ String query = "ASPECT:'cm:generalclassifiable' AND cm:name:'" + fileForLateAssignment.getName() + "'";
+ SearchResponse response = queryAsUser(testUser, query);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ boolean targetFound = response.getEntries().stream()
+ .anyMatch(e -> e.getModel().getName().equals(fileForLateAssignment.getName()));
+ Assert.assertFalse(targetFound,
+ "Expected uncategorised file to NOT match the cm:generalclassifiable aspect query at this point");
+ }
+
+ @Test(priority = 8)
+ public void testCategoryAddedToExistingFileGetsIndexed()
+ {
+ String categoryQuery = "cm:categories:\"" + primaryCategoryNodeRef + "\" AND cm:name:'" +
+ fileForLateAssignment.getName() + "'";
+ SearchResponse before = queryAsUser(testUser, categoryQuery);
+ boolean targetFoundBefore = before.getEntries().stream()
+ .anyMatch(e -> e.getModel().getName().equals(fileForLateAssignment.getName()));
+ Assert.assertFalse(targetFoundBefore,
+ "File should not be findable via the category before assignment");
+
+ JsonObject body = Json.createObjectBuilder()
+ .add("aspectNames", Json.createArrayBuilder().add("cm:generalclassifiable"))
+ .add("properties", Json.createObjectBuilder()
+ .add("cm:categories", Json.createArrayBuilder().add(primaryCategoryNodeRef)))
+ .build();
+ restClient.authenticateUser(testUser).withCoreAPI().usingNode(fileForLateAssignment).updateNode(body.toString());
+
+ Assert.assertTrue(isContentInSearchResults(categoryQuery, fileForLateAssignment.getName(), true),
+ "File should be findable via the category after assignment");
+ }
+
+ @Test(priority = 9)
+ public void testCategoryRemovalOnlyAffectsTargetedFile()
+ {
+ String secondaryPathClause = "PATH:\"/app:company_home/st:sites/cm:" + testSite.getTitle() +
+ "/cm:documentLibrary//*\"";
+
+ String queryBoth = "cm:categories:\"" + secondaryCategoryNodeRef + "\" AND " + secondaryPathClause;
+ SearchResponse both = queryAsUser(testUser, queryBoth);
+ Assert.assertTrue(both.getPagination().getCount() >= 2,
+ "Both isolation files should initially be findable via the secondary category");
+ Assert.assertTrue(isContentInSearchResponse(both, isolationFileA.getName()),
+ "Expected " + isolationFileA.getName() + " to be present before removal");
+ Assert.assertTrue(isContentInSearchResponse(both, isolationFileB.getName()),
+ "Expected " + isolationFileB.getName() + " to be present before removal");
+
+ JsonObject removeBody = Json.createObjectBuilder()
+ .add("properties", Json.createObjectBuilder()
+ .add("cm:categories", Json.createArrayBuilder()))
+ .build();
+ restClient.authenticateUser(testUser).withCoreAPI().usingNode(isolationFileA).updateNode(removeBody.toString());
+
+ String queryA = "cm:categories:\"" + secondaryCategoryNodeRef + "\" AND cm:name:'" + isolationFileA.getName() + "'";
+ Assert.assertTrue(isContentInSearchResults(queryA, isolationFileA.getName(), false),
+ "isolationFileA should no longer be findable via the secondary category after removal");
+
+ String queryB = "cm:categories:\"" + secondaryCategoryNodeRef + "\" AND cm:name:'" + isolationFileB.getName() + "'";
+ SearchResponse resultB = queryAsUser(testUser, queryB);
+ boolean targetBFound = resultB.getEntries().stream()
+ .anyMatch(e -> e.getModel().getName().equals(isolationFileB.getName()));
+ Assert.assertTrue(targetBFound,
+ "isolationFileB should still be findable via the secondary category — removal must not affect siblings");
+ }
+}
diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchCmisQueriesTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchCmisQueriesTest.java
new file mode 100644
index 0000000000..37aff9d2e9
--- /dev/null
+++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchCmisQueriesTest.java
@@ -0,0 +1,201 @@
+/*
+ * #%L
+ * Alfresco Search Services E2E Test
+ * %%
+ * Copyright (C) 2005 - 2026 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+
+package org.alfresco.rest.search;
+
+import org.springframework.http.HttpStatus;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.alfresco.utility.model.FileModel;
+import org.alfresco.utility.model.FolderModel;
+
+/**
+ * Migration test class for CMIS-language queries on Elasticsearch.
+ */
+public class SearchCmisQueriesTest extends AbstractSearchServicesE2ETest
+{
+ private FolderModel folder;
+ private FileModel invoice;
+ private FileModel report;
+ private FileModel memo;
+
+ private static final String UNIQUE_PREFIX = "acscmismig";
+
+ @BeforeClass(alwaysRun = true)
+ public void dataPreparation()
+ {
+ folder = dataContent.usingUser(testUser).usingSite(testSite).createFolderCmisApi(UNIQUE_PREFIX + "-folder");
+
+ invoice = new FileModel(UNIQUE_PREFIX + "-invoice-january.txt");
+ invoice.setContent("Invoice content covering multiple line items");
+ dataContent.usingUser(testUser).usingResource(folder).createContent(invoice);
+
+ report = new FileModel(UNIQUE_PREFIX + "-report-quarterly.txt");
+ report.setContent("Quarterly report of activities");
+ dataContent.usingUser(testUser).usingResource(folder).createContent(report);
+
+ memo = new FileModel(UNIQUE_PREFIX + "-memo-internal.txt");
+ memo.setContent("Internal memo for staff");
+ dataContent.usingUser(testUser).usingResource(folder).createContent(memo);
+
+ waitForContentIndexing(invoice.getContent(), true);
+ waitForContentIndexing(report.getContent(), true);
+ waitForContentIndexing(memo.getContent(), true);
+ }
+
+ private SearchResponse runCmisQuery(String cmisQuery)
+ {
+ SearchRequest searchRequest = new SearchRequest();
+ RestRequestQueryModel queryModel = new RestRequestQueryModel();
+ queryModel.setQuery(cmisQuery);
+ queryModel.setLanguage(SearchLanguage.CMIS.toString());
+ searchRequest.setQuery(queryModel);
+ return restClient.authenticateUser(testUser).withSearchAPI().search(searchRequest);
+ }
+
+ @Test(priority = 1)
+ public void testCmisSelectWithLike()
+ {
+ String cmisQuery = "SELECT * FROM cmis:document WHERE cmis:name LIKE '" + UNIQUE_PREFIX + "-invoice%'";
+ SearchResponse response = runCmisQuery(cmisQuery);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 1,
+ "Expected exactly one invoice file with the CMIS LIKE query");
+ Assert.assertTrue(isContentInSearchResponse(response, invoice.getName()),
+ "Expected the returned entry to be " + invoice.getName());
+ }
+
+ @Test(priority = 2)
+ public void testCmisWithMultipleWhereClauses()
+ {
+ String cmisQuery = "SELECT * FROM cmis:document WHERE cmis:name LIKE '" + UNIQUE_PREFIX +
+ "%' AND cmis:name LIKE '%report%'";
+ SearchResponse response = runCmisQuery(cmisQuery);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 1,
+ "Expected exactly the report file matching both LIKE conditions");
+ Assert.assertTrue(isContentInSearchResponse(response, report.getName()),
+ "Expected the returned entry to be " + report.getName());
+ }
+
+ @Test(priority = 3)
+ public void testCmisFullTextWithMetadata()
+ {
+ String cmisQuery = "SELECT * FROM cmis:document WHERE CONTAINS('quarterly') AND cmis:name LIKE '" +
+ UNIQUE_PREFIX + "%'";
+ SearchResponse response = runCmisQuery(cmisQuery);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(response.getPagination().getCount() >= 1,
+ "Expected at least one file matching CONTAINS + cmis:name filter");
+ Assert.assertTrue(isContentInSearchResponse(response, report.getName()),
+ "Expected " + report.getName() + " in the CONTAINS + metadata results");
+ }
+
+ @Test(priority = 4)
+ public void testCmisExactEqualityMatch()
+ {
+ String cmisQuery = "SELECT * FROM cmis:document WHERE cmis:name = '" + memo.getName() + "'";
+ SearchResponse response = runCmisQuery(cmisQuery);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 1,
+ "Expected exact-equality CMIS query to return the specific file");
+ Assert.assertTrue(isContentInSearchResponse(response, memo.getName()),
+ "Expected the returned entry to be " + memo.getName());
+ }
+
+ @Test(priority = 5)
+ public void testCmisWithOrClause()
+ {
+ String cmisQuery = "SELECT * FROM cmis:document WHERE cmis:name = '" + invoice.getName() +
+ "' OR cmis:name = '" + report.getName() + "'";
+ SearchResponse response = runCmisQuery(cmisQuery);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 2,
+ "Expected CMIS OR to return exactly two matching files");
+ Assert.assertTrue(isContentInSearchResponse(response, invoice.getName()),
+ "Expected " + invoice.getName() + " in the CMIS OR results");
+ Assert.assertTrue(isContentInSearchResponse(response, report.getName()),
+ "Expected " + report.getName() + " in the CMIS OR results");
+ }
+
+ @Test(priority = 6)
+ public void testCmisSelectFromCmisFolder()
+ {
+ String cmisQuery = "SELECT * FROM cmis:folder WHERE cmis:name = '" + folder.getName() + "'";
+ SearchResponse response = runCmisQuery(cmisQuery);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 1,
+ "Expected CMIS query on cmis:folder type to return the folder");
+ Assert.assertTrue(isContentInSearchResponse(response, folder.getName()),
+ "Expected the returned entry to be " + folder.getName());
+ }
+
+ @Test(priority = 7)
+ public void testCmisWithOrderBy()
+ {
+ String cmisQuery = "SELECT * FROM cmis:document WHERE cmis:name LIKE '" + UNIQUE_PREFIX +
+ "%' ORDER BY cmis:name ASC";
+ SearchResponse response = runCmisQuery(cmisQuery);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(response.getPagination().getCount() >= 3,
+ "Expected ORDER BY CMIS query to return the prefixed files");
+ Assert.assertTrue(isContentInSearchResponse(response, invoice.getName()),
+ "Expected " + invoice.getName() + " in the ORDER BY results");
+ Assert.assertTrue(isContentInSearchResponse(response, memo.getName()),
+ "Expected " + memo.getName() + " in the ORDER BY results");
+ Assert.assertTrue(isContentInSearchResponse(response, report.getName()),
+ "Expected " + report.getName() + " in the ORDER BY results");
+
+ String firstName = response.getEntries().getFirst().getModel().getName();
+ Assert.assertEquals(firstName, invoice.getName(),
+ "Expected the alphabetically first file (invoice) to be returned first");
+ }
+
+ @Test(priority = 8)
+ public void testCmisWithNotClause()
+ {
+ // Select all our test files but exclude anything matching '%invoice%'.
+ // Should return the report and memo files, but NOT the invoice.
+ String cmisQuery = "SELECT * FROM cmis:document WHERE cmis:name LIKE '" + UNIQUE_PREFIX +
+ "%' AND cmis:name NOT LIKE '%invoice%'";
+ SearchResponse response = runCmisQuery(cmisQuery);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 2,
+ "Expected NOT LIKE to exclude the invoice file and return exactly the report and memo files");
+ Assert.assertTrue(isContentInSearchResponse(response, report.getName()),
+ "Expected " + report.getName() + " in the NOT LIKE results");
+ Assert.assertTrue(isContentInSearchResponse(response, memo.getName()),
+ "Expected " + memo.getName() + " in the NOT LIKE results");
+
+ // Verify the invoice is truly excluded (guards against ES translator bug where NOT is ignored).
+ boolean invoiceInResults = response.getEntries().stream()
+ .anyMatch(e -> e.getModel().getName().equals(invoice.getName()));
+ Assert.assertFalse(invoiceInResults,
+ "Expected the invoice file to be excluded by NOT LIKE '%invoice%'");
+ }
+}
diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchComplexAclTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchComplexAclTest.java
new file mode 100644
index 0000000000..2577534803
--- /dev/null
+++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchComplexAclTest.java
@@ -0,0 +1,228 @@
+/*
+ * #%L
+ * Alfresco Search Services E2E Test
+ * %%
+ * Copyright (C) 2005 - 2026 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+
+package org.alfresco.rest.search;
+
+import jakarta.json.Json;
+import jakarta.json.JsonObject;
+
+import org.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.DataGroup;
+import org.alfresco.utility.model.FileModel;
+import org.alfresco.utility.model.FileType;
+import org.alfresco.utility.model.FolderModel;
+import org.alfresco.utility.model.GroupModel;
+import org.alfresco.utility.model.UserModel;
+
+/**
+ * Migration test class for complex ACL structures on Elasticsearch.
+ */
+public class SearchComplexAclTest extends AbstractSearchServicesE2ETest
+{
+ @Autowired
+ protected DataGroup dataGroup;
+
+ private UserModel groupUser;
+ private UserModel secondGroupUser;
+ private UserModel individualUser;
+ private UserModel outsiderUser;
+ private FolderModel folderA;
+ private FileModel groupProtectedFile;
+ private FileModel mixedPermsFile;
+ private FileModel inheritedProtectedFile;
+
+ @BeforeClass(alwaysRun = true)
+ public void dataPreparation()
+ {
+ groupUser = dataUser.createRandomTestUser("GroupMemberUser");
+ secondGroupUser = dataUser.createRandomTestUser("SecondGroupMemberUser");
+ individualUser = dataUser.createRandomTestUser("IndividualUser");
+ outsiderUser = dataUser.createRandomTestUser("OutsiderUser");
+
+ GroupModel authorizedGroup = dataGroup.createRandomGroup();
+ dataGroup.addListOfUsersToGroup(authorizedGroup, groupUser);
+ dataGroup.addListOfUsersToGroup(authorizedGroup, secondGroupUser);
+
+ dataUser.addUserToSite(groupUser, testSite, UserRole.SiteContributor);
+ dataUser.addUserToSite(secondGroupUser, testSite, UserRole.SiteContributor);
+ dataUser.addUserToSite(individualUser, testSite, UserRole.SiteContributor);
+ dataUser.addUserToSite(outsiderUser, testSite, UserRole.SiteContributor);
+
+ folderA = dataContent.usingUser(testUser).usingSite(testSite).createFolderCmisApi("acl-folder-a");
+ FolderModel folderB = dataContent.usingUser(testUser).usingSite(testSite).createFolderCmisApi("acl-folder-b");
+
+ groupProtectedFile = new FileModel("group-protected-file.txt", FileType.TEXT_PLAIN, "Group ACL protected");
+ mixedPermsFile = new FileModel("mixed-perms-file.txt", FileType.TEXT_PLAIN, "Mixed ALLOW/DENY ACL");
+ inheritedProtectedFile = new FileModel("inherited-protected-file.txt", FileType.TEXT_PLAIN, "Inherits parent ACL");
+
+ dataContent.usingUser(testUser).usingResource(folderA).createContent(groupProtectedFile);
+ dataContent.usingUser(testUser).usingResource(folderB).createContent(mixedPermsFile);
+
+ JsonObject folderAPerm = Json.createObjectBuilder()
+ .add("permissions", Json.createObjectBuilder()
+ .add("isInheritanceEnabled", false)
+ .add("locallySet", Json.createObjectBuilder()
+ .add("authorityId", "GROUP_" + authorizedGroup.getGroupIdentifier())
+ .add("name", "SiteContributor")
+ .add("accessStatus", "ALLOWED")))
+ .build();
+ restClient.authenticateUser(testUser).withCoreAPI().usingNode(folderA).updateNode(folderAPerm.toString());
+ restClient.authenticateUser(testUser).withCoreAPI().usingNode(groupProtectedFile).updateNode(folderAPerm.toString());
+
+ dataContent.usingUser(testUser).usingResource(folderA).createContent(inheritedProtectedFile);
+
+ JsonObject mixedPerm = Json.createObjectBuilder()
+ .add("permissions", Json.createObjectBuilder()
+ .add("isInheritanceEnabled", false)
+ .add("locallySet", Json.createArrayBuilder()
+ .add(Json.createObjectBuilder()
+ .add("authorityId", individualUser.getUsername())
+ .add("name", "SiteContributor")
+ .add("accessStatus", "ALLOWED"))
+ .add(Json.createObjectBuilder()
+ .add("authorityId", outsiderUser.getUsername())
+ .add("name", "SiteContributor")
+ .add("accessStatus", "DENIED"))))
+ .build();
+ restClient.authenticateUser(testUser).withCoreAPI().usingNode(mixedPermsFile).updateNode(mixedPerm.toString());
+
+ waitForMetadataIndexing(groupProtectedFile.getName(), true);
+ waitForMetadataIndexing(mixedPermsFile.getName(), true);
+ waitForMetadataIndexing(inheritedProtectedFile.getName(), true);
+ }
+
+ @Test(priority = 1)
+ public void testGroupBasedPermissions()
+ {
+ SearchResponse groupMember = queryAsUser(groupUser, "cm:name:'" + groupProtectedFile.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(groupMember.getPagination().getCount(), 1,
+ "Group member should find the group-protected file");
+ Assert.assertTrue(isContentInSearchResponse(groupMember, groupProtectedFile.getName()),
+ "Expected the returned entry to be " + groupProtectedFile.getName());
+
+ SearchResponse outsider = queryAsUser(outsiderUser, "cm:name:'" + groupProtectedFile.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(outsider.getPagination().getCount(), 0,
+ "Non-group user should NOT find the group-protected file");
+ }
+
+ @Test(priority = 2)
+ public void testInheritedGroupPermissionOnFolder()
+ {
+ SearchResponse groupMember = queryAsUser(groupUser, "cm:name:'" + inheritedProtectedFile.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(groupMember.getPagination().getCount(), 1,
+ "Group member should find the file that inherits the folder ACL");
+ Assert.assertTrue(isContentInSearchResponse(groupMember, inheritedProtectedFile.getName()),
+ "Expected the returned entry to be " + inheritedProtectedFile.getName());
+
+ SearchResponse outsider = queryAsUser(outsiderUser, "cm:name:'" + inheritedProtectedFile.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(outsider.getPagination().getCount(), 0,
+ "Non-group user should not find the inherited-permission file");
+ }
+
+ @Test(priority = 3)
+ public void testMixedAllowDenyOnSameNode()
+ {
+ SearchResponse allowed = queryAsUser(individualUser, "cm:name:'" + mixedPermsFile.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(allowed.getPagination().getCount(), 1,
+ "Explicitly ALLOWED user should find the file");
+ Assert.assertTrue(isContentInSearchResponse(allowed, mixedPermsFile.getName()),
+ "Expected the returned entry to be " + mixedPermsFile.getName());
+
+ SearchResponse denied = queryAsUser(outsiderUser, "cm:name:'" + mixedPermsFile.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(denied.getPagination().getCount(), 0,
+ "Explicitly DENIED user should not find the file");
+ }
+
+ @Test(priority = 4)
+ public void testAdminSeesAllRegardlessOfAcl()
+ {
+ SearchResponse groupProtected = queryAsUser(dataUser.getAdminUser(),
+ "cm:name:'" + groupProtectedFile.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(groupProtected.getPagination().getCount() >= 1,
+ "Admin should find the group-protected file regardless of ACL");
+ Assert.assertTrue(isContentInSearchResponse(groupProtected, groupProtectedFile.getName()),
+ "Expected " + groupProtectedFile.getName() + " in the admin results");
+
+ SearchResponse mixedPerms = queryAsUser(dataUser.getAdminUser(),
+ "cm:name:'" + mixedPermsFile.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(mixedPerms.getPagination().getCount() >= 1,
+ "Admin should also find the mixed-permission file");
+ Assert.assertTrue(isContentInSearchResponse(mixedPerms, mixedPermsFile.getName()),
+ "Expected " + mixedPermsFile.getName() + " in the admin results");
+ }
+
+ @Test(priority = 5)
+ public void testMultipleUsersInGroupSeeSameFile()
+ {
+ SearchResponse firstMember = queryAsUser(groupUser, "cm:name:'" + groupProtectedFile.getName() + "'");
+ SearchResponse secondMember = queryAsUser(secondGroupUser, "cm:name:'" + groupProtectedFile.getName() + "'");
+
+ Assert.assertEquals(firstMember.getPagination().getCount(), 1,
+ "First group member should find the group-protected file");
+ Assert.assertTrue(isContentInSearchResponse(firstMember, groupProtectedFile.getName()),
+ "Expected first member's result to be " + groupProtectedFile.getName());
+
+ Assert.assertEquals(secondMember.getPagination().getCount(), 1,
+ "Second group member should also find the group-protected file");
+ Assert.assertTrue(isContentInSearchResponse(secondMember, groupProtectedFile.getName()),
+ "Expected second member's result to be " + groupProtectedFile.getName());
+ }
+
+ @Test(priority = 6)
+ public void testNewFileInProtectedFolderInheritsGroupAcl()
+ {
+ FileModel newlyCreatedFile = new FileModel("newly-created-in-folder-a.txt", FileType.TEXT_PLAIN,
+ "Created after ACL was applied");
+ dataContent.usingUser(testUser).usingResource(folderA).createContent(newlyCreatedFile);
+ waitForMetadataIndexing(newlyCreatedFile.getName(), true);
+
+ SearchResponse groupMember = queryAsUser(groupUser, "cm:name:'" + newlyCreatedFile.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(groupMember.getPagination().getCount(), 1,
+ "Group member should find the newly created file inheriting the folder ACL");
+ Assert.assertTrue(isContentInSearchResponse(groupMember, newlyCreatedFile.getName()),
+ "Expected the returned entry to be " + newlyCreatedFile.getName());
+
+ SearchResponse outsider = queryAsUser(outsiderUser, "cm:name:'" + newlyCreatedFile.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(outsider.getPagination().getCount(), 0,
+ "Non-group user should not find the newly created file inheriting the folder ACL");
+ }
+}
diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchDeepFolderHierarchyTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchDeepFolderHierarchyTest.java
new file mode 100644
index 0000000000..8925d13836
--- /dev/null
+++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchDeepFolderHierarchyTest.java
@@ -0,0 +1,245 @@
+/*
+ * #%L
+ * Alfresco Search Services E2E Test
+ * %%
+ * Copyright (C) 2005 - 2026 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+
+package org.alfresco.rest.search;
+
+import org.springframework.http.HttpStatus;
+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.FolderModel;
+
+/**
+ * Migration test class for deep folder hierarchies (path indexing verification) on Elasticsearch.
+ */
+public class SearchDeepFolderHierarchyTest extends AbstractSearchServicesE2ETest
+{
+ private FolderModel levelOne;
+ private FolderModel levelTwo;
+ private FolderModel levelThree;
+ private FolderModel levelFour;
+ private FolderModel levelFive;
+ private FolderModel levelSix;
+ private FileModel deepFile;
+ private FileModel additionalDeepFileOne;
+ private FileModel additionalDeepFileTwo;
+
+ private String pathBase;
+
+ @BeforeClass(alwaysRun = true)
+ public void dataPreparation()
+ {
+ levelOne = dataContent.usingUser(testUser).usingSite(testSite)
+ .createFolderCmisApi("level-one-folder");
+ levelTwo = dataContent.usingUser(testUser).usingSite(testSite).usingResource(levelOne)
+ .createFolderCmisApi("level-two-folder");
+ levelThree = dataContent.usingUser(testUser).usingSite(testSite).usingResource(levelTwo)
+ .createFolderCmisApi("level-three-folder");
+ levelFour = dataContent.usingUser(testUser).usingSite(testSite).usingResource(levelThree)
+ .createFolderCmisApi("level-four-folder");
+ levelFive = dataContent.usingUser(testUser).usingSite(testSite).usingResource(levelFour)
+ .createFolderCmisApi("level-five-folder");
+ levelSix = dataContent.usingUser(testUser).usingSite(testSite).usingResource(levelFive)
+ .createFolderCmisApi("level-six-folder");
+
+ deepFile = new FileModel("deep-nested-file.txt");
+ deepFile.setContent("content at the deepest level");
+ dataContent.usingUser(testUser).usingResource(levelSix).createContent(deepFile);
+
+ additionalDeepFileOne = new FileModel("deep-nested-file-two.txt");
+ additionalDeepFileOne.setContent("second file in the same deepest folder");
+ dataContent.usingUser(testUser).usingResource(levelSix).createContent(additionalDeepFileOne);
+
+ additionalDeepFileTwo = new FileModel("deep-nested-file-three.txt");
+ additionalDeepFileTwo.setContent("third file in the same deepest folder");
+ dataContent.usingUser(testUser).usingResource(levelSix).createContent(additionalDeepFileTwo);
+
+ pathBase = "/app:company_home/st:sites/cm:" + testSite.getTitle() + "/cm:documentLibrary";
+
+ waitForMetadataIndexing(deepFile.getName(), true);
+ waitForMetadataIndexing(additionalDeepFileOne.getName(), true);
+ waitForMetadataIndexing(additionalDeepFileTwo.getName(), true);
+ }
+
+ @Test(priority = 1)
+ public void testPathQueryAtDeepLevel()
+ {
+ String deepPath = pathBase +
+ "/cm:" + levelOne.getName() +
+ "/cm:" + levelTwo.getName() +
+ "/cm:" + levelThree.getName() +
+ "/cm:" + levelFour.getName() +
+ "/cm:" + levelFive.getName() +
+ "/cm:" + levelSix.getName() +
+ "/cm:" + deepFile.getName();
+
+ SearchResponse response = queryAsUser(testUser, "PATH:\"" + deepPath + "\"");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 1,
+ "Expected the deeply-nested file to be found at its exact path");
+ Assert.assertTrue(isContentInSearchResponse(response, deepFile.getName()),
+ "Expected the returned entry to be " + deepFile.getName());
+ }
+
+ @Test(priority = 2)
+ public void testWildcardPathAcrossLevels()
+ {
+ String wildcardPath = pathBase +
+ "/cm:" + levelOne.getName() +
+ "//*";
+
+ SearchResponse response = queryAsUser(testUser,
+ "PATH:\"" + wildcardPath + "\" AND cm:name:'" + deepFile.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 1,
+ "Expected the deeply-nested file to be found via wildcard path");
+ Assert.assertTrue(isContentInSearchResponse(response, deepFile.getName()),
+ "Expected the returned entry to be " + deepFile.getName());
+ }
+
+ @Test(priority = 3)
+ public void testPathQueryAtIntermediateLevel()
+ {
+ String intermediatePath = pathBase +
+ "/cm:" + levelOne.getName() +
+ "/cm:" + levelTwo.getName() +
+ "/cm:" + levelThree.getName();
+
+ SearchResponse response = queryAsUser(testUser, "PATH:\"" + intermediatePath + "\"");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 1,
+ "Expected the intermediate-level folder to be found at its path");
+ Assert.assertTrue(isContentInSearchResponse(response, levelThree.getName()),
+ "Expected the returned entry to be " + levelThree.getName());
+ }
+
+ @Test(priority = 4)
+ public void testParentQueryAtDeepLevel()
+ {
+ String deepestFolderPath = pathBase +
+ "/cm:" + levelOne.getName() +
+ "/cm:" + levelTwo.getName() +
+ "/cm:" + levelThree.getName() +
+ "/cm:" + levelFour.getName() +
+ "/cm:" + levelFive.getName() +
+ "/cm:" + levelSix.getName();
+
+ // Poll until the deepest folder is indexed, so the lookup below can't hit an empty result.
+ Assert.assertTrue(isContentInSearchResults("PATH:\"" + deepestFolderPath + "\"", levelSix.getName(), true),
+ "Deepest folder should be findable via PATH before running the PARENT query");
+
+ SearchResponse folderLookup = queryAsUser(testUser, "PATH:\"" + deepestFolderPath + "\"");
+ Assert.assertFalse(folderLookup.getEntries().isEmpty(),
+ "PATH lookup for the deepest folder returned no entries");
+
+ String folderId = folderLookup.getEntries().getFirst().getModel().getId();
+
+ String parentQuery = "PARENT:'workspace://SpacesStore/" + folderId +
+ "' AND cm:name:'" + deepFile.getName() + "'";
+
+ SearchResponse response = queryAsUser(testUser, parentQuery);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 1,
+ "Expected the file to be found as the direct child of the deepest folder");
+ Assert.assertTrue(isContentInSearchResponse(response, deepFile.getName()),
+ "Expected the returned entry to be " + deepFile.getName());
+ }
+
+ @Test(priority = 5)
+ public void testPathQueryAtRootOfHierarchy()
+ {
+ String rootPath = pathBase + "/cm:" + levelOne.getName();
+ SearchResponse response = queryAsUser(testUser, "PATH:\"" + rootPath + "\"");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 1,
+ "Expected the level-one folder to be found at the root of the hierarchy");
+ Assert.assertTrue(isContentInSearchResponse(response, levelOne.getName()),
+ "Expected the returned entry to be " + levelOne.getName());
+ }
+
+ @Test(priority = 6)
+ public void testMultipleFilesInSameDeepFolder()
+ {
+ String deepFolderPath = pathBase +
+ "/cm:" + levelOne.getName() +
+ "/cm:" + levelTwo.getName() +
+ "/cm:" + levelThree.getName() +
+ "/cm:" + levelFour.getName() +
+ "/cm:" + levelFive.getName() +
+ "/cm:" + levelSix.getName() +
+ "/*";
+
+ SearchResponse response = queryAsUser(testUser, "PATH:\"" + deepFolderPath + "\"");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 3,
+ "Expected all three files at the deepest level to be found");
+ Assert.assertTrue(isContentInSearchResponse(response, deepFile.getName()),
+ "Expected " + deepFile.getName() + " among the deepest-folder children");
+ Assert.assertTrue(isContentInSearchResponse(response, additionalDeepFileOne.getName()),
+ "Expected " + additionalDeepFileOne.getName() + " among the deepest-folder children");
+ Assert.assertTrue(isContentInSearchResponse(response, additionalDeepFileTwo.getName()),
+ "Expected " + additionalDeepFileTwo.getName() + " among the deepest-folder children");
+ }
+
+ @Test(priority = 7)
+ public void testTypeAndPathCombined()
+ {
+ String deepFolderPath = pathBase +
+ "/cm:" + levelOne.getName() +
+ "//*";
+
+ String query = "TYPE:'cm:content' AND PATH:\"" + deepFolderPath + "\"";
+ SearchResponse response = queryAsUser(testUser, query);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(response.getPagination().getCount() >= 3,
+ "Expected at least the three files under the hierarchy to be returned by TYPE+PATH");
+ Assert.assertTrue(isContentInSearchResponse(response, deepFile.getName()),
+ "Expected " + deepFile.getName() + " in the TYPE+PATH results");
+ Assert.assertTrue(isContentInSearchResponse(response, additionalDeepFileOne.getName()),
+ "Expected " + additionalDeepFileOne.getName() + " in the TYPE+PATH results");
+ Assert.assertTrue(isContentInSearchResponse(response, additionalDeepFileTwo.getName()),
+ "Expected " + additionalDeepFileTwo.getName() + " in the TYPE+PATH results");
+ }
+
+ @Test(priority = 8)
+ public void testDirectChildrenViaPathWildcard()
+ {
+ String midLevelChildrenPath = pathBase +
+ "/cm:" + levelOne.getName() +
+ "/cm:" + levelTwo.getName() +
+ "/cm:" + levelThree.getName() +
+ "/*";
+
+ SearchResponse response = queryAsUser(testUser, "PATH:\"" + midLevelChildrenPath + "\"");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 1,
+ "Expected exactly the level-four folder as the direct child of level-three");
+ Assert.assertTrue(isContentInSearchResponse(response, levelFour.getName()),
+ "Expected the returned entry to be " + levelFour.getName());
+ }
+}
diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchMultiLanguageTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchMultiLanguageTest.java
new file mode 100644
index 0000000000..2631f1ac53
--- /dev/null
+++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchMultiLanguageTest.java
@@ -0,0 +1,143 @@
+/*
+ * #%L
+ * Alfresco Search Services E2E Test
+ * %%
+ * Copyright (C) 2005 - 2026 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+
+package org.alfresco.rest.search;
+
+import org.springframework.http.HttpStatus;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.alfresco.utility.model.FileModel;
+import org.alfresco.utility.model.FolderModel;
+
+/**
+ * Migration test class for multi-language content search on Elasticsearch. Uses only default ES analyzer behaviour — no cross-locale settings required.
+ */
+public class SearchMultiLanguageTest extends AbstractSearchServicesE2ETest
+{
+ private FileModel frenchContent;
+ private FileModel spanishContent;
+ private FileModel mixedLanguageContent;
+
+ private static final String UNIQUE_PREFIX = "acsmiglang";
+
+ @BeforeClass(alwaysRun = true)
+ public void dataPreparation()
+ {
+ FolderModel folder = dataContent.usingUser(testUser).usingSite(testSite)
+ .createFolderCmisApi(UNIQUE_PREFIX + "-folder");
+
+ frenchContent = new FileModel(UNIQUE_PREFIX + "-french-menu.txt");
+ frenchContent.setContent("Le café propose des croissants et des baguettes traditionnelles.");
+ dataContent.usingUser(testUser).usingResource(folder).createContent(frenchContent);
+
+ spanishContent = new FileModel(UNIQUE_PREFIX + "-spanish-note.txt");
+ spanishContent.setContent("El niño está jugando en el jardín con su mamá.");
+ dataContent.usingUser(testUser).usingResource(folder).createContent(spanishContent);
+
+ mixedLanguageContent = new FileModel(UNIQUE_PREFIX + "-mixed-language.txt");
+ mixedLanguageContent.setContent("Hello world. Bonjour tout le monde. Hola mundo. Guten Tag.");
+ dataContent.usingUser(testUser).usingResource(folder).createContent(mixedLanguageContent);
+
+ waitForContentIndexing(frenchContent.getContent(), true);
+ waitForContentIndexing(spanishContent.getContent(), true);
+ waitForContentIndexing(mixedLanguageContent.getContent(), true);
+ }
+
+ @Test(priority = 1)
+ public void testFrenchContentSearchByAsciiTerm()
+ {
+ SearchResponse response = queryAsUser(testUser,
+ "cm:content:'croissants' AND cm:name:'" + frenchContent.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 1,
+ "Expected French content to be found via an ASCII term inside it");
+ Assert.assertTrue(isContentInSearchResponse(response, frenchContent.getName()),
+ "Expected the returned entry to be " + frenchContent.getName());
+ }
+
+ @Test(priority = 2)
+ public void testSpanishContentSearchByAsciiTerm()
+ {
+ SearchResponse response = queryAsUser(testUser,
+ "cm:content:'jugando' AND cm:name:'" + spanishContent.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 1,
+ "Expected Spanish content to be found via an ASCII term inside it");
+ Assert.assertTrue(isContentInSearchResponse(response, spanishContent.getName()),
+ "Expected the returned entry to be " + spanishContent.getName());
+ }
+
+ @Test(priority = 3)
+ public void testMixedLanguageFullText()
+ {
+ SearchResponse english = queryAsUser(testUser,
+ "cm:content:'Hello' AND cm:name:'" + mixedLanguageContent.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(english.getPagination().getCount(), 1, "Expected file found via English term");
+ Assert.assertTrue(isContentInSearchResponse(english, mixedLanguageContent.getName()),
+ "Expected the English-term result to be " + mixedLanguageContent.getName());
+
+ SearchResponse spanish = queryAsUser(testUser,
+ "cm:content:'mundo' AND cm:name:'" + mixedLanguageContent.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(spanish.getPagination().getCount(), 1, "Expected file found via Spanish term");
+ Assert.assertTrue(isContentInSearchResponse(spanish, mixedLanguageContent.getName()),
+ "Expected the Spanish-term result to be " + mixedLanguageContent.getName());
+
+ SearchResponse german = queryAsUser(testUser,
+ "cm:content:'Guten' AND cm:name:'" + mixedLanguageContent.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(german.getPagination().getCount(), 1, "Expected file found via German term");
+ Assert.assertTrue(isContentInSearchResponse(german, mixedLanguageContent.getName()),
+ "Expected the German-term result to be " + mixedLanguageContent.getName());
+ }
+
+ @Test(priority = 4)
+ public void testCommonWordFoundAcrossMultipleFiles()
+ {
+ SearchResponse response = queryAsUser(testUser,
+ "cm:content:'Hola' AND cm:name:'" + UNIQUE_PREFIX + "*'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(response.getPagination().getCount() >= 1,
+ "Expected the shared Spanish greeting to be found in at least one migration test file");
+ Assert.assertTrue(isContentInSearchResponse(response, mixedLanguageContent.getName()),
+ "Expected " + mixedLanguageContent.getName() + " to contain the shared Spanish greeting");
+ }
+
+ @Test(priority = 5)
+ public void testWildcardSearchOnMultilingualContent()
+ {
+ SearchResponse response = queryAsUser(testUser,
+ "cm:content:'crois*' AND cm:name:'" + frenchContent.getName() + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 1,
+ "Expected wildcard search to hit 'croissants' in the French content file");
+ Assert.assertTrue(isContentInSearchResponse(response, frenchContent.getName()),
+ "Expected the returned entry to be " + frenchContent.getName());
+ }
+}
diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchSecondaryAssociationAdvancedTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchSecondaryAssociationAdvancedTest.java
new file mode 100644
index 0000000000..76d76c8ef5
--- /dev/null
+++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchSecondaryAssociationAdvancedTest.java
@@ -0,0 +1,251 @@
+/*
+ * #%L
+ * Alfresco Search Services E2E Test
+ * %%
+ * Copyright (C) 2005 - 2026 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+
+package org.alfresco.rest.search;
+
+import 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.data.CustomObjectTypeProperties;
+import org.alfresco.utility.model.FileModel;
+import org.alfresco.utility.model.FolderModel;
+
+/**
+ * Migration test class for advanced secondary parent/child association scenarios on Elasticsearch.
+ */
+public class SearchSecondaryAssociationAdvancedTest extends AbstractSearchServicesE2ETest
+{
+ private FolderModel primaryFolder;
+ private FolderModel secondaryFolderA;
+ private FolderModel secondaryFolderB;
+ private FolderModel nestedParent;
+ private FolderModel nestedChild;
+ private FolderModel commonSecondaryFolder;
+ private FileModel file;
+ private FileModel nestedTargetFile;
+ private FileModel siblingFileOne;
+ private FileModel siblingFileTwo;
+
+ private String pathBase;
+
+ @BeforeClass(alwaysRun = true)
+ public void dataPreparation()
+ {
+ primaryFolder = new FolderModel("primary-parent-folder");
+ secondaryFolderA = new FolderModel("secondary-parent-folder-a");
+ secondaryFolderB = new FolderModel("secondary-parent-folder-b");
+ nestedParent = new FolderModel("nested-parent-folder");
+ nestedChild = new FolderModel("nested-child-folder");
+ commonSecondaryFolder = new FolderModel("common-secondary-folder");
+
+ file = new FileModel("multi-secondary-file.txt");
+ file.setContent("File that will have multiple secondary parents");
+ nestedTargetFile = new FileModel("nested-secondary-file.txt");
+ nestedTargetFile.setContent("File to be secondary-associated to a nested folder");
+ siblingFileOne = new FileModel("sibling-file-one.txt");
+ siblingFileTwo = new FileModel("sibling-file-two.txt");
+
+ dataContent.usingUser(testUser).usingSite(testSite)
+ .createCustomContent(primaryFolder, "cmis:folder", new CustomObjectTypeProperties());
+ dataContent.usingUser(testUser).usingSite(testSite)
+ .createCustomContent(secondaryFolderA, "cmis:folder", new CustomObjectTypeProperties());
+ dataContent.usingUser(testUser).usingSite(testSite)
+ .createCustomContent(secondaryFolderB, "cmis:folder", new CustomObjectTypeProperties());
+ dataContent.usingUser(testUser).usingSite(testSite)
+ .createCustomContent(nestedParent, "cmis:folder", new CustomObjectTypeProperties());
+ dataContent.usingUser(testUser).usingResource(nestedParent)
+ .createCustomContent(nestedChild, "cmis:folder", new CustomObjectTypeProperties());
+ dataContent.usingUser(testUser).usingSite(testSite)
+ .createCustomContent(commonSecondaryFolder, "cmis:folder", new CustomObjectTypeProperties());
+
+ dataContent.usingUser(testUser).usingResource(primaryFolder)
+ .createCustomContent(file, "cmis:document", new CustomObjectTypeProperties());
+ dataContent.usingUser(testUser).usingResource(primaryFolder)
+ .createCustomContent(nestedTargetFile, "cmis:document", new CustomObjectTypeProperties());
+ dataContent.usingUser(testUser).usingResource(primaryFolder)
+ .createCustomContent(siblingFileOne, "cmis:document", new CustomObjectTypeProperties());
+ dataContent.usingUser(testUser).usingResource(primaryFolder)
+ .createCustomContent(siblingFileTwo, "cmis:document", new CustomObjectTypeProperties());
+
+ pathBase = "/app:company_home/st:sites/cm:" + testSite.getTitle() + "/cm:documentLibrary";
+
+ waitForMetadataIndexing(file.getName(), true);
+ waitForMetadataIndexing(nestedTargetFile.getName(), true);
+ waitForMetadataIndexing(siblingFileOne.getName(), true);
+ waitForMetadataIndexing(siblingFileTwo.getName(), true);
+ }
+
+ @Test(priority = 1)
+ public void testMultipleSecondaryAssociationsInSameSite() throws EmptyRestModelCollectionException
+ {
+ RestNodeChildAssociationModel assocA = new RestNodeChildAssociationModel(file.getNodeRefWithoutVersion(), "cm:contains");
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderA).addSecondaryChildren(assocA);
+
+ RestNodeChildAssociationModel assocB = new RestNodeChildAssociationModel(file.getNodeRefWithoutVersion(), "cm:contains");
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderB).addSecondaryChildren(assocB);
+
+ String pathViaA = pathBase + "/cm:" + secondaryFolderA.getName() + "/cm:" + file.getName();
+ String pathViaB = pathBase + "/cm:" + secondaryFolderB.getName() + "/cm:" + file.getName();
+ String pathViaPrimary = pathBase + "/cm:" + primaryFolder.getName() + "/cm:" + file.getName();
+
+ Assert.assertTrue(isContentInSearchResults("PATH:\"" + pathViaA + "\"", file.getName(), true),
+ "File not findable via first secondary parent path");
+ Assert.assertTrue(isContentInSearchResults("PATH:\"" + pathViaB + "\"", file.getName(), true),
+ "File not findable via second secondary parent path");
+ Assert.assertTrue(isContentInSearchResults("PATH:\"" + pathViaPrimary + "\"", file.getName(), true),
+ "File not findable via primary parent path");
+
+ RestNodeAssociationModelCollection secChildrenA = restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderA).getSecondaryChildren();
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderA).removeSecondaryChild(secChildrenA.getEntryByIndex(0));
+
+ RestNodeAssociationModelCollection secChildrenB = restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderB).getSecondaryChildren();
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderB).removeSecondaryChild(secChildrenB.getEntryByIndex(0));
+ }
+
+ @Test(priority = 2)
+ public void testNestedSecondaryPathQuery() throws EmptyRestModelCollectionException
+ {
+ RestNodeChildAssociationModel assoc = new RestNodeChildAssociationModel(nestedTargetFile.getNodeRefWithoutVersion(), "cm:contains");
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(nestedChild).addSecondaryChildren(assoc);
+
+ String pathViaNestedSecondary = pathBase +
+ "/cm:" + nestedParent.getName() +
+ "/cm:" + nestedChild.getName() +
+ "/cm:" + nestedTargetFile.getName();
+
+ Assert.assertTrue(isContentInSearchResults("PATH:\"" + pathViaNestedSecondary + "\"", nestedTargetFile.getName(), true),
+ "File not findable via nested secondary parent path");
+
+ RestNodeAssociationModelCollection secChildren = restClient.authenticateUser(testUser).withCoreAPI().usingResource(nestedChild).getSecondaryChildren();
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(nestedChild).removeSecondaryChild(secChildren.getEntryByIndex(0));
+ }
+
+ @Test(priority = 3)
+ public void testSecondaryAssociationRemovalUpdatesIndex() throws EmptyRestModelCollectionException
+ {
+ RestNodeChildAssociationModel assoc = new RestNodeChildAssociationModel(file.getNodeRefWithoutVersion(), "cm:contains");
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderA).addSecondaryChildren(assoc);
+
+ String pathViaSecondary = pathBase + "/cm:" + secondaryFolderA.getName() + "/cm:" + file.getName();
+ Assert.assertTrue(isContentInSearchResults("PATH:\"" + pathViaSecondary + "\"", file.getName(), true),
+ "File should be findable after secondary association is added");
+
+ RestNodeAssociationModelCollection secChildren = restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderA).getSecondaryChildren();
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderA).removeSecondaryChild(secChildren.getEntryByIndex(0));
+
+ Assert.assertTrue(isContentInSearchResults("PATH:\"" + pathViaSecondary + "\"", file.getName(), false),
+ "File should NOT be findable after secondary association is removed");
+ }
+
+ @Test(priority = 4)
+ public void testFileFoundOnceGloballyDespiteMultipleAssociations() throws EmptyRestModelCollectionException
+ {
+ RestNodeChildAssociationModel assocA = new RestNodeChildAssociationModel(file.getNodeRefWithoutVersion(), "cm:contains");
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderA).addSecondaryChildren(assocA);
+ RestNodeChildAssociationModel assocB = new RestNodeChildAssociationModel(file.getNodeRefWithoutVersion(), "cm:contains");
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderB).addSecondaryChildren(assocB);
+
+ SearchResponse response = queryAsUser(testUser, "cm:name:'" + file.getName() + "'");
+ Assert.assertEquals(response.getPagination().getCount(), 1,
+ "Node with multiple secondary parents should still be indexed as a single entry");
+ Assert.assertTrue(isContentInSearchResponse(response, file.getName()),
+ "Expected the single returned entry to be " + file.getName());
+
+ RestNodeAssociationModelCollection secChildrenA = restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderA).getSecondaryChildren();
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderA).removeSecondaryChild(secChildrenA.getEntryByIndex(0));
+ RestNodeAssociationModelCollection secChildrenB = restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderB).getSecondaryChildren();
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderB).removeSecondaryChild(secChildrenB.getEntryByIndex(0));
+ }
+
+ @Test(priority = 5)
+ public void testMultipleFilesInSameSecondaryFolder() throws EmptyRestModelCollectionException
+ {
+ RestNodeChildAssociationModel assoc1 = new RestNodeChildAssociationModel(siblingFileOne.getNodeRefWithoutVersion(), "cm:contains");
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(commonSecondaryFolder).addSecondaryChildren(assoc1);
+ RestNodeChildAssociationModel assoc2 = new RestNodeChildAssociationModel(siblingFileTwo.getNodeRefWithoutVersion(), "cm:contains");
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(commonSecondaryFolder).addSecondaryChildren(assoc2);
+
+ String pathViaCommon = pathBase + "/cm:" + commonSecondaryFolder.getName() + "/*";
+
+ // Wait for the secondary-association PATH to be indexed for both files
+ // (batch-indexing catches up on association changes with a small lag).
+ Assert.assertTrue(isContentInSearchResults("PATH:\"" + pathViaCommon + "\"", siblingFileOne.getName(), true),
+ "First file should be findable via the common secondary folder path");
+ Assert.assertTrue(isContentInSearchResults("PATH:\"" + pathViaCommon + "\"", siblingFileTwo.getName(), true),
+ "Second file should be findable via the common secondary folder path");
+
+ SearchResponse response = queryAsUser(testUser, "PATH:\"" + pathViaCommon + "\"");
+ Assert.assertEquals(response.getPagination().getCount(), 2,
+ "Both files should be findable via the common secondary folder path");
+ Assert.assertTrue(isContentInSearchResponse(response, siblingFileOne.getName()),
+ "Expected " + siblingFileOne.getName() + " among the common secondary folder children");
+ Assert.assertTrue(isContentInSearchResponse(response, siblingFileTwo.getName()),
+ "Expected " + siblingFileTwo.getName() + " among the common secondary folder children");
+
+ RestNodeAssociationModelCollection secChildren = restClient.authenticateUser(testUser).withCoreAPI().usingResource(commonSecondaryFolder).getSecondaryChildren();
+ while (!secChildren.getEntries().isEmpty())
+ {
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(commonSecondaryFolder).removeSecondaryChild(secChildren.getEntryByIndex(0));
+ secChildren = restClient.authenticateUser(testUser).withCoreAPI().usingResource(commonSecondaryFolder).getSecondaryChildren();
+ }
+ }
+
+ @Test(priority = 6)
+ public void testPathViaPrimaryStillWorksAfterSecondaryAdded() throws EmptyRestModelCollectionException
+ {
+ RestNodeChildAssociationModel assoc = new RestNodeChildAssociationModel(file.getNodeRefWithoutVersion(), "cm:contains");
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderA).addSecondaryChildren(assoc);
+
+ String pathViaPrimary = pathBase + "/cm:" + primaryFolder.getName() + "/cm:" + file.getName();
+ Assert.assertTrue(isContentInSearchResults("PATH:\"" + pathViaPrimary + "\"", file.getName(), true),
+ "File must still be findable via primary parent path after a secondary association is added");
+
+ RestNodeAssociationModelCollection secChildren = restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderA).getSecondaryChildren();
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(secondaryFolderA).removeSecondaryChild(secChildren.getEntryByIndex(0));
+ }
+
+ @Test(priority = 7)
+ public void testSecondaryAssociationOnDeeplyNestedFileFoundViaSecondaryPath() throws EmptyRestModelCollectionException
+ {
+ RestNodeChildAssociationModel assoc = new RestNodeChildAssociationModel(nestedTargetFile.getNodeRefWithoutVersion(), "cm:contains");
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(nestedChild).addSecondaryChildren(assoc);
+
+ String secondaryDeepPath = pathBase +
+ "/cm:" + nestedParent.getName() +
+ "/cm:" + nestedChild.getName() +
+ "/cm:" + nestedTargetFile.getName();
+
+ Assert.assertTrue(isContentInSearchResults("PATH:\"" + secondaryDeepPath + "\"", nestedTargetFile.getName(), true),
+ "File must be findable at its secondary deep path");
+
+ RestNodeAssociationModelCollection secChildren = restClient.authenticateUser(testUser).withCoreAPI().usingResource(nestedChild).getSecondaryChildren();
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(nestedChild).removeSecondaryChild(secChildren.getEntryByIndex(0));
+ }
+}
diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchTagsTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchTagsTest.java
new file mode 100644
index 0000000000..b541a6f893
--- /dev/null
+++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/search/SearchTagsTest.java
@@ -0,0 +1,192 @@
+/*
+ * #%L
+ * Alfresco Search Services E2E Test
+ * %%
+ * Copyright (C) 2005 - 2026 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ * #L%
+ */
+
+package org.alfresco.rest.search;
+
+import org.springframework.http.HttpStatus;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.alfresco.utility.model.FileModel;
+import org.alfresco.utility.model.FolderModel;
+
+/**
+ * Migration test class for tag-based search scenarios on Elasticsearch.
+ */
+public class SearchTagsTest extends AbstractSearchServicesE2ETest
+{
+ private FileModel fileWithSingleTag;
+ private FileModel fileWithMultipleTags;
+ private FileModel anotherFileWithTagOne;
+ private FileModel fileWithAllThreeTags;
+
+ private static final String TAG_PREFIX = "acsmigrationtag";
+ private static final String TAG_ONE = TAG_PREFIX + "one";
+ private static final String TAG_TWO = TAG_PREFIX + "two";
+ private static final String TAG_THREE = TAG_PREFIX + "three";
+
+ @BeforeClass(alwaysRun = true)
+ public void dataPreparation()
+ {
+ FolderModel folder = dataContent.usingUser(testUser).usingSite(testSite).createFolderCmisApi("tags-folder");
+
+ fileWithSingleTag = new FileModel("file-with-single-tag.txt");
+ fileWithSingleTag.setContent("File with a single tag");
+ dataContent.usingUser(testUser).usingResource(folder).createContent(fileWithSingleTag);
+
+ fileWithMultipleTags = new FileModel("file-with-multiple-tags.txt");
+ fileWithMultipleTags.setContent("File with multiple tags");
+ dataContent.usingUser(testUser).usingResource(folder).createContent(fileWithMultipleTags);
+
+ anotherFileWithTagOne = new FileModel("another-file-with-tag-one.txt");
+ anotherFileWithTagOne.setContent("Second file also tagged with TAG_ONE");
+ dataContent.usingUser(testUser).usingResource(folder).createContent(anotherFileWithTagOne);
+
+ fileWithAllThreeTags = new FileModel("file-with-all-three-tags.txt");
+ fileWithAllThreeTags.setContent("File tagged with all three migration tags");
+ dataContent.usingUser(testUser).usingResource(folder).createContent(fileWithAllThreeTags);
+
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(fileWithSingleTag).addTag(TAG_ONE);
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(fileWithMultipleTags).addTag(TAG_TWO);
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(fileWithMultipleTags).addTag(TAG_THREE);
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(anotherFileWithTagOne).addTag(TAG_ONE);
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(fileWithAllThreeTags).addTag(TAG_ONE);
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(fileWithAllThreeTags).addTag(TAG_TWO);
+ restClient.authenticateUser(testUser).withCoreAPI().usingResource(fileWithAllThreeTags).addTag(TAG_THREE);
+
+ waitForMetadataIndexing(fileWithSingleTag.getName(), true);
+ waitForMetadataIndexing(fileWithMultipleTags.getName(), true);
+ waitForMetadataIndexing(anotherFileWithTagOne.getName(), true);
+ waitForMetadataIndexing(fileWithAllThreeTags.getName(), true);
+
+ // Wait until the TAG index catches up — batch-indexing needs time after tag-aspect
+ // and cm:categories property changes before TAG queries start returning results.
+ Assert.assertTrue(isContentInSearchResults("TAG:'" + TAG_ONE + "'", fileWithSingleTag.getName(), true),
+ "Setup: TAG_ONE should be searchable after batch-indexing catches up");
+ Assert.assertTrue(isContentInSearchResults("TAG:'" + TAG_TWO + "'", fileWithMultipleTags.getName(), true),
+ "Setup: TAG_TWO should be searchable after batch-indexing catches up");
+ Assert.assertTrue(isContentInSearchResults("TAG:'" + TAG_THREE + "'", fileWithMultipleTags.getName(), true),
+ "Setup: TAG_THREE should be searchable after batch-indexing catches up");
+ }
+
+ @Test(priority = 1)
+ public void testSearchByTag()
+ {
+ SearchResponse response = queryAsUser(testUser, "TAG:'" + TAG_ONE + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(response.getPagination().getCount() >= 1,
+ "Expected at least one file tagged with " + TAG_ONE);
+ Assert.assertTrue(isContentInSearchResponse(response, fileWithSingleTag.getName()),
+ "Expected " + fileWithSingleTag.getName() + " to be returned for TAG_ONE");
+ }
+
+ @Test(priority = 2)
+ public void testSearchByMultipleTagsOnSameFile()
+ {
+ SearchResponse responseTwo = queryAsUser(testUser, "TAG:'" + TAG_TWO + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(responseTwo.getPagination().getCount() >= 1,
+ "Expected the multi-tagged file to be findable via TAG_TWO");
+ Assert.assertTrue(isContentInSearchResponse(responseTwo, fileWithMultipleTags.getName()),
+ "Expected " + fileWithMultipleTags.getName() + " to be returned for TAG_TWO");
+
+ SearchResponse responseThree = queryAsUser(testUser, "TAG:'" + TAG_THREE + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(responseThree.getPagination().getCount() >= 1,
+ "Expected the multi-tagged file to be findable via TAG_THREE");
+ Assert.assertTrue(isContentInSearchResponse(responseThree, fileWithMultipleTags.getName()),
+ "Expected " + fileWithMultipleTags.getName() + " to be returned for TAG_THREE");
+ }
+
+ @Test(priority = 3)
+ public void testTagAndNameCombined()
+ {
+ String query = "TAG:'" + TAG_ONE + "' AND cm:name:'" + fileWithSingleTag.getName() + "'";
+ SearchResponse response = queryAsUser(testUser, query);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertEquals(response.getPagination().getCount(), 1,
+ "Expected exactly the one tagged file with matching name");
+ Assert.assertTrue(isContentInSearchResponse(response, fileWithSingleTag.getName()),
+ "Expected the returned entry to be " + fileWithSingleTag.getName());
+ }
+
+ @Test(priority = 4)
+ public void testTagWithWildcard()
+ {
+ SearchResponse response = queryAsUser(testUser, "TAG:'" + TAG_PREFIX + "*'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(response.getPagination().getCount() >= 3,
+ "Expected wildcard TAG query to find all files sharing the migration tag prefix");
+ Assert.assertTrue(isContentInSearchResponse(response, fileWithSingleTag.getName()),
+ "Expected " + fileWithSingleTag.getName() + " in the wildcard TAG results");
+ Assert.assertTrue(isContentInSearchResponse(response, fileWithMultipleTags.getName()),
+ "Expected " + fileWithMultipleTags.getName() + " in the wildcard TAG results");
+ Assert.assertTrue(isContentInSearchResponse(response, fileWithAllThreeTags.getName()),
+ "Expected " + fileWithAllThreeTags.getName() + " in the wildcard TAG results");
+ }
+
+ @Test(priority = 5)
+ public void testTagsWithConjunction()
+ {
+ String query = "TAG:'" + TAG_TWO + "' AND TAG:'" + TAG_THREE + "'";
+ SearchResponse response = queryAsUser(testUser, query);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(response.getPagination().getCount() >= 1,
+ "Expected at least one file tagged with both TAG_TWO and TAG_THREE");
+ Assert.assertTrue(isContentInSearchResponse(response, fileWithMultipleTags.getName()),
+ "Expected " + fileWithMultipleTags.getName() + " to satisfy the TAG_TWO AND TAG_THREE conjunction");
+ }
+
+ @Test(priority = 6)
+ public void testTagsWithDisjunction()
+ {
+ String query = "TAG:'" + TAG_ONE + "' OR TAG:'" + TAG_TWO + "'";
+ SearchResponse response = queryAsUser(testUser, query);
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(response.getPagination().getCount() >= 3,
+ "Expected disjunction TAG query to return files matching either tag");
+ Assert.assertTrue(isContentInSearchResponse(response, fileWithSingleTag.getName()),
+ "Expected " + fileWithSingleTag.getName() + " (TAG_ONE) in the disjunction results");
+ Assert.assertTrue(isContentInSearchResponse(response, fileWithMultipleTags.getName()),
+ "Expected " + fileWithMultipleTags.getName() + " (TAG_TWO) in the disjunction results");
+ }
+
+ @Test(priority = 7)
+ public void testMultipleFilesShareTag()
+ {
+ SearchResponse response = queryAsUser(testUser, "TAG:'" + TAG_ONE + "'");
+ restClient.assertStatusCodeIs(HttpStatus.OK);
+ Assert.assertTrue(response.getPagination().getCount() >= 3,
+ "Expected TAG_ONE to be shared by at least three files");
+ Assert.assertTrue(isContentInSearchResponse(response, fileWithSingleTag.getName()),
+ "Expected " + fileWithSingleTag.getName() + " to share TAG_ONE");
+ Assert.assertTrue(isContentInSearchResponse(response, anotherFileWithTagOne.getName()),
+ "Expected " + anotherFileWithTagOne.getName() + " to share TAG_ONE");
+ Assert.assertTrue(isContentInSearchResponse(response, fileWithAllThreeTags.getName()),
+ "Expected " + fileWithAllThreeTags.getName() + " to share TAG_ONE");
+ }
+}
diff --git a/packaging/tests/tas-restapi/src/test/resources/test-suites/elasticsearch-e2e-part1-suite.xml b/packaging/tests/tas-restapi/src/test/resources/test-suites/elasticsearch-e2e-part1-suite.xml
new file mode 100644
index 0000000000..b6b294e9bf
--- /dev/null
+++ b/packaging/tests/tas-restapi/src/test/resources/test-suites/elasticsearch-e2e-part1-suite.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packaging/tests/tas-restapi/src/test/resources/elasticsearch-e2e-suite.xml b/packaging/tests/tas-restapi/src/test/resources/test-suites/elasticsearch-e2e-part2-suite.xml
similarity index 50%
rename from packaging/tests/tas-restapi/src/test/resources/elasticsearch-e2e-suite.xml
rename to packaging/tests/tas-restapi/src/test/resources/test-suites/elasticsearch-e2e-part2-suite.xml
index fc5b30c26b..2696c08105 100644
--- a/packaging/tests/tas-restapi/src/test/resources/elasticsearch-e2e-suite.xml
+++ b/packaging/tests/tas-restapi/src/test/resources/test-suites/elasticsearch-e2e-part2-suite.xml
@@ -1,63 +1,75 @@
-
+
-
-
+
-
-
+
+
-
+
+
+
-
+
+
-
+
+
-
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -66,73 +78,19 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packaging/tests/tas-restapi/src/test/resources/test-suites/elasticsearch-e2e-part3-suite.xml b/packaging/tests/tas-restapi/src/test/resources/test-suites/elasticsearch-e2e-part3-suite.xml
new file mode 100644
index 0000000000..669d8a7a89
--- /dev/null
+++ b/packaging/tests/tas-restapi/src/test/resources/test-suites/elasticsearch-e2e-part3-suite.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packaging/tests/tas-restapi/src/test/resources/test-suites/part2-suite.xml b/packaging/tests/tas-restapi/src/test/resources/test-suites/part2-suite.xml
index 14e5417245..83754e0600 100644
--- a/packaging/tests/tas-restapi/src/test/resources/test-suites/part2-suite.xml
+++ b/packaging/tests/tas-restapi/src/test/resources/test-suites/part2-suite.xml
@@ -20,10 +20,11 @@
+
diff --git a/packaging/tests/tas-webdav/pom.xml b/packaging/tests/tas-webdav/pom.xml
index 3bea1c3c30..f9ceae73f0 100644
--- a/packaging/tests/tas-webdav/pom.xml
+++ b/packaging/tests/tas-webdav/pom.xml
@@ -9,7 +9,7 @@
org.alfresco
alfresco-community-repo-tests
- 26.3.0.36-SNAPSHOT
+ 26.3.0.50-SNAPSHOT
diff --git a/packaging/war/pom.xml b/packaging/war/pom.xml
index eaf3dfb1d9..f2c8f6b470 100644
--- a/packaging/war/pom.xml
+++ b/packaging/war/pom.xml
@@ -7,7 +7,7 @@
org.alfresco
alfresco-community-repo-packaging
- 26.3.0.36-SNAPSHOT
+ 26.3.0.50-SNAPSHOT
diff --git a/pom.xml b/pom.xml
index cd8c58449e..80da3e4c9c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
4.0.0
alfresco-community-repo
- 26.3.0.36-SNAPSHOT
+ 26.3.0.50-SNAPSHOT
pom
Alfresco Community Repo Parent
@@ -51,20 +51,21 @@
8.0.1
5.23.0
5.23.0
- 5.4.4-A.6
- 4.4.4-A.7
+ 5.4.4
+ 4.4.4
7.1
1.1.0-A.1
1.9.22.1
7.0.8
7.0.6
+ 1.16.6
3.5.3
2.22.1
4.1.7
1.0.0-jakarta-1
10.2
- 1.84
+ 1.85
5.18.0
1.18
3.27.3
@@ -75,9 +76,9 @@
33.3.1-jre
4.5.14
4.4.16
- 5.5
- 5.3.4
- 5.3.4
+ 5.6.4
+ 5.4.3
+ 5.4.3
3.1-HTTPCLIENT-1265
2.12.2
26.84.0
@@ -93,7 +94,7 @@
3.5.0.Final
4.18.3
4.1.137.Final
- 6.2.4
+ 6.2.7
1.27.1
4.2.2
@@ -123,9 +124,9 @@
1.1.8
2.9.0
2.5.2
- 5.0.0-A2
+ 5.0.0
3.4.2-A.3
- 26.2.0
+ 26.3.0-A.4
2.2.0
2.4.0
@@ -1055,6 +1056,13 @@
pom
import
+
+ io.micrometer
+ micrometer-bom
+ ${dependency.micrometer.version}
+ pom
+ import
+