From 0197b0e221339d561aa2e527d698e836aface28c Mon Sep 17 00:00:00 2001 From: krdabrowski <98942253+krdabrowski@users.noreply.github.com> Date: Fri, 13 Jan 2023 18:10:46 +0100 Subject: [PATCH] ACS-4033: List linked categories for a particular node (#1672) * ACS-4033: List linked categories for a particular node - GET /nodes/{nodeId}/category-links --- .../rest/model/RestCategoryLinkBodyModel.java | 48 ++++ .../java/org/alfresco/rest/requests/Node.java | 11 + .../rest/categories/CategoriesRestTest.java | 46 +++- .../categories/LinkToCategoriesTests.java | 83 +++---- .../ListCategoriesForNodeTests.java | 220 ++++++++++++++++++ .../org/alfresco/rest/api/Categories.java | 12 +- .../NodesCategoryLinksRelation.java | 22 +- .../rest/api/impl/CategoriesImpl.java | 29 ++- .../resource/SerializablePagedCollection.java | 1 - .../resource/parameters/ArrayListPage.java | 4 +- .../resource/parameters/ListPage.java | 5 +- .../NodesCategoryLinksRelationTest.java | 91 ++++++++ .../rest/api/impl/CategoriesImplTest.java | 94 +++++++- .../parameters/ArrayListPageTest.java | 4 +- 14 files changed, 605 insertions(+), 65 deletions(-) create mode 100644 packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/ListCategoriesForNodeTests.java create mode 100644 remote-api/src/test/java/org/alfresco/rest/api/categories/NodesCategoryLinksRelationTest.java diff --git a/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/model/RestCategoryLinkBodyModel.java b/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/model/RestCategoryLinkBodyModel.java index 2b5f6fe245..3154b3c7d8 100644 --- a/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/model/RestCategoryLinkBodyModel.java +++ b/packaging/tests/tas-restapi/src/main/java/org/alfresco/rest/model/RestCategoryLinkBodyModel.java @@ -1,5 +1,7 @@ package org.alfresco.rest.model; +import java.util.Objects; + import com.fasterxml.jackson.annotation.JsonProperty; import org.alfresco.rest.core.IRestModel; import org.alfresco.utility.model.TestModel; @@ -36,5 +38,51 @@ public class RestCategoryLinkBodyModel extends TestModel implements IRestModel return restWrapper.processModel(RestRuleExecutionModel.class, request); } + /** + * Get linked categories performing GET cal on "/nodes/{nodeId}/category-links" + * + * @return categories which are linked from content + */ + public RestCategoryModelsCollection getLinkedCategories() + { + RestRequest request = RestRequest.simpleRequest(HttpMethod.GET, "nodes/{nodeId}/category-links", repoModel.getNodeRef()); + return restWrapper.processModels(RestCategoryModelsCollection.class, request); + } + /** * Link content to category performing POST call on "/nodes/{nodeId}/category-links" * diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/CategoriesRestTest.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/CategoriesRestTest.java index 660729bfb6..f424f5724b 100644 --- a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/CategoriesRestTest.java +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/CategoriesRestTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2022 Alfresco Software Limited + * Copyright (C) 2005 - 2023 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -30,8 +30,14 @@ import static org.alfresco.utility.data.RandomData.getRandomName; import static org.alfresco.utility.report.log.Step.STEP; import static org.springframework.http.HttpStatus.CREATED; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + import org.alfresco.rest.RestTest; +import org.alfresco.rest.model.RestCategoryLinkBodyModel; import org.alfresco.rest.model.RestCategoryModel; +import org.alfresco.utility.model.RepoTestModel; import org.alfresco.utility.model.UserModel; import org.testng.annotations.BeforeClass; @@ -71,6 +77,30 @@ abstract class CategoriesRestTest extends RestTest return createdCategory; } + protected List prepareCategoriesUnderRoot(final int categoriesCount) + { + return prepareCategoriesUnder(ROOT_CATEGORY_ID, categoriesCount); + } + + protected List prepareCategoriesUnder(final String parentId, final int categoriesCount) + { + final RestCategoryModel parentCategory = createCategoryModelWithId(parentId); + final List categoryModels = IntStream + .range(0, categoriesCount) + .mapToObj(i -> createCategoryModelWithName(getRandomName(CATEGORY_NAME_PREFIX))) + .collect(Collectors.toList()); + final List createdCategories = restClient.authenticateUser(dataUser.getAdminUser()) + .withCoreAPI() + .usingCategory(parentCategory) + .createCategoriesList(categoryModels) + .getEntries().stream() + .map(RestCategoryModel::onModel) + .collect(Collectors.toList()); + restClient.assertStatusCodeIs(CREATED); + + return createdCategories; + } + protected RestCategoryModel createCategoryModelWithId(final String id) { return createCategoryModelWithIdAndName(id, null); @@ -88,4 +118,18 @@ abstract class CategoriesRestTest extends RestTest .name(name) .create(); } + + protected RestCategoryLinkBodyModel createCategoryLinkModelWithId(final String id) + { + return RestCategoryLinkBodyModel.builder() + .categoryId(id) + .create(); + } + + protected RepoTestModel createNodeModelWithId(final String id) + { + final RepoTestModel nodeModel = new RepoTestModel() {}; + nodeModel.setNodeRef(id); + return nodeModel; + } } diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/LinkToCategoriesTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/LinkToCategoriesTests.java index c26fc31415..a47f7e04f9 100644 --- a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/LinkToCategoriesTests.java +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/LinkToCategoriesTests.java @@ -60,13 +60,13 @@ public class LinkToCategoriesTests extends CategoriesRestTest private static final String ASPECTS_FIELD = "aspectNames"; private static final String PROPERTIES_FIELD = "properties"; - private UserModel user; private SiteModel site; private FolderModel folder; private FileModel file; private RestCategoryModel category; @BeforeClass(alwaysRun = true) + @Override public void dataPreparation() { STEP("Create user and a site"); @@ -96,8 +96,8 @@ public class LinkToCategoriesTests extends CategoriesRestTest fileNode.assertThat().field(PROPERTIES_FIELD).notContains("cm:categories"); STEP("Link content to created category and expect 201"); - final RestCategoryLinkBodyModel categoryLink = createCategoryLinkWithId(category.getId()); - final RestCategoryModel linkedCategory = restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategory(categoryLink); + final RestCategoryLinkBodyModel categoryLinkModel = createCategoryLinkModelWithId(category.getId()); + final RestCategoryModel linkedCategory = restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategory(categoryLinkModel); restClient.assertStatusCodeIs(CREATED); linkedCategory.assertThat().isEqualTo(category); @@ -126,11 +126,11 @@ public class LinkToCategoriesTests extends CategoriesRestTest final RestCategoryModel secondCategory = prepareCategoryUnderRoot(); STEP("Link content to created categories and expect 201"); - final List categoryLinks = List.of( - createCategoryLinkWithId(category.getId()), - createCategoryLinkWithId(secondCategory.getId()) + final List categoryLinkModels = List.of( + createCategoryLinkModelWithId(category.getId()), + createCategoryLinkModelWithId(secondCategory.getId()) ); - final RestCategoryModelsCollection linkedCategories = restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategories(categoryLinks); + final RestCategoryModelsCollection linkedCategories = restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategories(categoryLinkModels); restClient.assertStatusCodeIs(CREATED); linkedCategories.getEntries().get(0).onModel().assertThat().isEqualTo(category); @@ -152,18 +152,18 @@ public class LinkToCategoriesTests extends CategoriesRestTest public void testLinkContentToCategory_usingContentWithAlreadyLinkedCategories() { STEP("Link content to created category"); - final RestCategoryLinkBodyModel categoryLink = createCategoryLinkWithId(category.getId()); - restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategory(categoryLink); + final RestCategoryLinkBodyModel categoryLinkModel = createCategoryLinkModelWithId(category.getId()); + restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategory(categoryLinkModel); restClient.assertStatusCodeIs(CREATED); STEP("Create second and third category under root, link content to them and expect 201"); final RestCategoryModel secondCategory = prepareCategoryUnderRoot(); final RestCategoryModel thirdCategory = prepareCategoryUnderRoot(); - final List categoryLinks = List.of( - createCategoryLinkWithId(secondCategory.getId()), - createCategoryLinkWithId(thirdCategory.getId()) + final List categoryLinkModels = List.of( + createCategoryLinkModelWithId(secondCategory.getId()), + createCategoryLinkModelWithId(thirdCategory.getId()) ); - final RestCategoryModelsCollection linkedCategories = restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategories(categoryLinks); + final RestCategoryModelsCollection linkedCategories = restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategories(categoryLinkModels); restClient.assertStatusCodeIs(CREATED); linkedCategories.assertThat().entriesListCountIs(2); @@ -186,9 +186,9 @@ public class LinkToCategoriesTests extends CategoriesRestTest public void testLinkContentToCategory_asUserWithoutReadPermissionAndExpect403() { STEP("Try to link content to a category using user without read permission and expect 403"); - final RestCategoryLinkBodyModel categoryLink = createCategoryLinkWithId(category.getId()); + final RestCategoryLinkBodyModel categoryLinkModel = createCategoryLinkModelWithId(category.getId()); final UserModel userWithoutRights = dataUser.createRandomTestUser(); - restClient.authenticateUser(userWithoutRights).withCoreAPI().usingNode(file).linkToCategory(categoryLink); + restClient.authenticateUser(userWithoutRights).withCoreAPI().usingNode(file).linkToCategory(categoryLinkModel); restClient.assertStatusCodeIs(FORBIDDEN); } @@ -201,11 +201,11 @@ public class LinkToCategoriesTests extends CategoriesRestTest { STEP("Create another user as a consumer for file"); final UserModel consumer = dataUser.createRandomTestUser(); - addPermissionsForUser(consumer.getUsername(), "Consumer", file); + allowPermissionsForUser(consumer.getUsername(), "Consumer", file); STEP("Try to link content to a category using user without change permission and expect 403"); - final RestCategoryLinkBodyModel categoryLink = createCategoryLinkWithId(category.getId()); - restClient.authenticateUser(consumer).withCoreAPI().usingNode(file).linkToCategory(categoryLink); + final RestCategoryLinkBodyModel categoryLinkModel = createCategoryLinkModelWithId(category.getId()); + restClient.authenticateUser(consumer).withCoreAPI().usingNode(file).linkToCategory(categoryLinkModel); restClient.assertStatusCodeIs(FORBIDDEN); } @@ -226,8 +226,8 @@ public class LinkToCategoriesTests extends CategoriesRestTest dataUser.removeUserFromSite(user, privateSite); STEP("Try to link content to a category as owner and expect 201"); - final RestCategoryLinkBodyModel categoryLink = createCategoryLinkWithId(category.getId()); - restClient.authenticateUser(user).withCoreAPI().usingNode(privateFile).linkToCategory(categoryLink); + final RestCategoryLinkBodyModel categoryLinkModel = createCategoryLinkModelWithId(category.getId()); + restClient.authenticateUser(user).withCoreAPI().usingNode(privateFile).linkToCategory(categoryLinkModel); restClient.assertStatusCodeIs(CREATED); } @@ -240,8 +240,8 @@ public class LinkToCategoriesTests extends CategoriesRestTest { STEP("Try to link content to non-existing category and expect 404"); final String nonExistingCategoryId = "non-existing-dummy-id"; - final RestCategoryLinkBodyModel categoryLink = createCategoryLinkWithId(nonExistingCategoryId); - restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategory(categoryLink); + final RestCategoryLinkBodyModel categoryLinkModel = createCategoryLinkModelWithId(nonExistingCategoryId); + restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategory(categoryLinkModel); restClient.assertStatusCodeIs(NOT_FOUND); } @@ -266,8 +266,8 @@ public class LinkToCategoriesTests extends CategoriesRestTest { STEP("Try to call link content API with empty category ID and expect 400"); final String nonExistingCategoryId = StringUtils.EMPTY; - final RestCategoryLinkBodyModel categoryLink = createCategoryLinkWithId(nonExistingCategoryId); - restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategory(categoryLink); + final RestCategoryLinkBodyModel categoryLinkModel = createCategoryLinkModelWithId(nonExistingCategoryId); + restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategory(categoryLinkModel); restClient.assertStatusCodeIs(BAD_REQUEST); } @@ -279,8 +279,8 @@ public class LinkToCategoriesTests extends CategoriesRestTest public void testLinkFolderToCategory() { STEP("Link folder node to category"); - final RestCategoryLinkBodyModel categoryLink = createCategoryLinkWithId(category.getId()); - restClient.authenticateUser(user).withCoreAPI().usingNode(folder).linkToCategory(categoryLink); + final RestCategoryLinkBodyModel categoryLinkModel = createCategoryLinkModelWithId(category.getId()); + restClient.authenticateUser(user).withCoreAPI().usingNode(folder).linkToCategory(categoryLinkModel); restClient.assertStatusCodeIs(CREATED); } @@ -292,11 +292,10 @@ public class LinkToCategoriesTests extends CategoriesRestTest public void testLinkContentToCategory_usingTagInsteadOfContentAndExpect405() { STEP("Try to link a tag to category and expect 405"); - final RestCategoryLinkBodyModel categoryLink = createCategoryLinkWithId(category.getId()); + final RestCategoryLinkBodyModel categoryLinkModel = createCategoryLinkModelWithId(category.getId()); final RestTagModel tag = restClient.authenticateUser(user).withCoreAPI().usingNode(file).addTag("someTag"); - final RepoTestModel tagNode = new RepoTestModel() {}; - tagNode.setNodeRef(tag.getId()); - restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI().usingNode(tagNode).linkToCategory(categoryLink); + final RepoTestModel tagNode = createNodeModelWithId(tag.getId()); + restClient.authenticateUser(dataUser.getAdminUser()).withCoreAPI().usingNode(tagNode).linkToCategory(categoryLinkModel); restClient.assertStatusCodeIs(METHOD_NOT_ALLOWED); } @@ -308,8 +307,8 @@ public class LinkToCategoriesTests extends CategoriesRestTest public void testLinkContentToCategory_usingFolderInsteadOfCategoryAndExpect400() { STEP("Try to link content to non-category and expect 400"); - final RestCategoryLinkBodyModel categoryLink = createCategoryLinkWithId(folder.getNodeRef()); - restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategory(categoryLink); + final RestCategoryLinkBodyModel categoryLinkModel = createCategoryLinkModelWithId(folder.getNodeRef()); + restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategory(categoryLinkModel); restClient.assertStatusCodeIs(BAD_REQUEST); } @@ -321,30 +320,22 @@ public class LinkToCategoriesTests extends CategoriesRestTest public void testLinkContentToCategory_usingRootCategoryAndExpect400() { STEP("Try to link content to root category and expect 400"); - final RestCategoryLinkBodyModel categoryLink = createCategoryLinkWithId(ROOT_CATEGORY_ID); - restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategory(categoryLink); + final RestCategoryLinkBodyModel categoryLinkModel = createCategoryLinkModelWithId(ROOT_CATEGORY_ID); + restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategory(categoryLinkModel); restClient.assertStatusCodeIs(BAD_REQUEST); } - private RestCategoryLinkBodyModel createCategoryLinkWithId(final String id) - { - final RestCategoryLinkBodyModel categoryLink = new RestCategoryLinkBodyModel(); - categoryLink.setCategoryId(id); - return categoryLink; - } - - private void addPermissionsForUser(final String username, final String role, final FileModel file) + private void allowPermissionsForUser(final String username, final String role, final FileModel file) { final String putPermissionsBody = Json.createObjectBuilder().add("permissions", Json.createObjectBuilder() .add("isInheritanceEnabled", true) .add("locallySet", Json.createObjectBuilder() .add("authorityId", username) - .add("name", role).add("accessStatus", "ALLOWED"))) - .build() - .toString(); - + .add("name", role) + .add("accessStatus", "ALLOWED"))) + .build().toString(); restClient.authenticateUser(user).withCoreAPI().usingNode(file).updateNode(putPermissionsBody); } } diff --git a/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/ListCategoriesForNodeTests.java b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/ListCategoriesForNodeTests.java new file mode 100644 index 0000000000..be311704bf --- /dev/null +++ b/packaging/tests/tas-restapi/src/test/java/org/alfresco/rest/categories/ListCategoriesForNodeTests.java @@ -0,0 +1,220 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2023 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.categories; + +import static org.alfresco.utility.report.log.Step.STEP; +import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.METHOD_NOT_ALLOWED; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.OK; + +import javax.json.Json; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.alfresco.dataprep.CMISUtil; +import org.alfresco.rest.model.RestCategoryLinkBodyModel; +import org.alfresco.rest.model.RestCategoryModel; +import org.alfresco.rest.model.RestCategoryModelsCollection; +import org.alfresco.rest.model.RestTagModel; +import org.alfresco.utility.model.FileModel; +import org.alfresco.utility.model.FolderModel; +import org.alfresco.utility.model.RepoTestModel; +import org.alfresco.utility.model.SiteModel; +import org.alfresco.utility.model.TestGroup; +import org.alfresco.utility.model.UserModel; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class ListCategoriesForNodeTests extends CategoriesRestTest +{ + + private SiteModel site; + private FolderModel folder; + private FileModel file; + private RestCategoryModel category; + + @BeforeClass(alwaysRun = true) + @Override + public void dataPreparation() + { + STEP("Create user and a site"); + user = dataUser.createRandomTestUser(); + site = dataSite.usingUser(user).createPublicRandomSite(); + } + + @BeforeMethod(alwaysRun = true) + public void setUp() + { + STEP("Create a folder, file in it and a category under root"); + folder = dataContent.usingUser(user).usingSite(site).createFolder(); + file = dataContent.usingUser(user).usingResource(folder).createContent(CMISUtil.DocumentType.TEXT_PLAIN); + category = prepareCategoryUnderRoot(); + } + + /** + * Get one linked category using file + */ + @Test(groups = { TestGroup.REST_API}) + public void testListSingleCategoryForNode_usingFile() + { + STEP("Link file to category"); + final RestCategoryLinkBodyModel categoryLink = createCategoryLinkModelWithId(category.getId()); + final RestCategoryModel linkedCategory = restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategory(categoryLink); + + STEP("Get linked category"); + final RestCategoryModelsCollection linkedCategories = restClient.authenticateUser(user).withCoreAPI().usingNode(file).getLinkedCategories(); + + restClient.assertStatusCodeIs(OK); + linkedCategories.assertThat().entriesListCountIs(1); + linkedCategories.getEntries().get(0).onModel().assertThat().isEqualTo(linkedCategory); + } + + /** + * Get one linked category using folder + */ + @Test(groups = { TestGroup.REST_API}) + public void testListSingleCategoryForNode_usingFolder() + { + STEP("Link folder to category"); + final RestCategoryLinkBodyModel categoryLink = createCategoryLinkModelWithId(category.getId()); + final RestCategoryModel linkedCategory = restClient.authenticateUser(user).withCoreAPI().usingNode(folder).linkToCategory(categoryLink); + + STEP("Get linked category"); + final RestCategoryModelsCollection linkedCategories = restClient.authenticateUser(user).withCoreAPI().usingNode(folder).getLinkedCategories(); + + restClient.assertStatusCodeIs(OK); + linkedCategories.assertThat().entriesListCountIs(1); + linkedCategories.getEntries().get(0).onModel().assertThat().isEqualTo(linkedCategory); + } + + /** + * Get multiple linked categories using file + */ + @Test(groups = { TestGroup.REST_API}) + public void testListMultipleCategoriesForNode_usingFile() + { + STEP("Create multiple categories under root"); + final List createdCategories = prepareCategoriesUnderRoot(10); + + STEP("Link file to created categories"); + final List categoryLinkModels = createdCategories.stream() + .map(RestCategoryModel::getId) + .map(this::createCategoryLinkModelWithId) + .collect(Collectors.toList()); + final List createdCategoryLinks = restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategories( + categoryLinkModels + ).getEntries(); + + STEP("Get categories which are linked from file and compare them to created category links"); + final RestCategoryModelsCollection linkedCategories = restClient.authenticateUser(user).withCoreAPI().usingNode(file).getLinkedCategories(); + + restClient.assertStatusCodeIs(OK); + linkedCategories.assertThat().entriesListCountIs(createdCategoryLinks.size()); + IntStream.range(0, createdCategoryLinks.size()).forEach(i -> + linkedCategories.getEntries().get(i).onModel().assertThat().isEqualTo(createdCategoryLinks.get(i).onModel()) + ); + } + + /** + * Try to get linked categories for content which is not linked to any category + */ + @Test(groups = { TestGroup.REST_API}) + public void testListCategoriesForNode_withoutLinkedCategories() + { + STEP("Try to get linked categories and expect empty list"); + final RestCategoryModelsCollection linkedCategories = restClient.authenticateUser(user).withCoreAPI().usingNode(file).getLinkedCategories(); + + restClient.assertStatusCodeIs(OK); + linkedCategories.assertThat().entriesListIsEmpty(); + } + + /** + * Try to get linked categories using non-existing node and expect 404 (Not Found) + */ + @Test(groups = { TestGroup.REST_API}) + public void testListCategoriesForNode_usingNonExistingNodeAndExpect404() + { + STEP("Try to get linked categories for non-existing node and expect 404"); + final RepoTestModel nonExistingNode = createNodeModelWithId("non-existing-id"); + final RestCategoryModelsCollection linkedCategories = restClient.authenticateUser(user).withCoreAPI().usingNode(nonExistingNode).getLinkedCategories(); + + restClient.assertStatusCodeIs(NOT_FOUND); + linkedCategories.assertThat().entriesListIsEmpty(); + } + + /** + * Try to get multiple linked categories as user without read permission and expect 403 (Forbidden) + */ + @Test(groups = { TestGroup.REST_API}) + public void testListCategoriesForNode_asUserWithoutReadPermissionAndExpect403() + { + STEP("Link content to category"); + final RestCategoryLinkBodyModel categoryLink = createCategoryLinkModelWithId(category.getId()); + restClient.authenticateUser(user).withCoreAPI().usingNode(file).linkToCategory(categoryLink); + + STEP("Create another user and deny consumer rights"); + final UserModel userWithoutRights = dataUser.createRandomTestUser(); + denyPermissionsForUser(userWithoutRights.getUsername(), "Consumer", file); + + STEP("Try to get linked categories using user without read permission and expect 403"); + restClient.authenticateUser(userWithoutRights).withCoreAPI().usingNode(file).getLinkedCategories(); + + restClient.assertStatusCodeIs(FORBIDDEN); + } + + /** + * Try to get linked categories using tag instead of a content and expect 405 (Method Not Allowed) + */ + @Test(groups = { TestGroup.REST_API}) + public void testListCategoriesForNode_usingTagInsteadOfContentAndExpect405() + { + STEP("Add tag to file"); + final RestTagModel tag = restClient.authenticateUser(user).withCoreAPI().usingNode(file).addTag("someTag"); + final RepoTestModel tagNode = createNodeModelWithId(tag.getId()); + + STEP("Try to get linked categories for a tag and expect 405"); + restClient.authenticateUser(user).withCoreAPI().usingNode(tagNode).getLinkedCategories(); + + restClient.assertStatusCodeIs(METHOD_NOT_ALLOWED); + } + + private void denyPermissionsForUser(final String username, final String role, final FileModel file) + { + final String putPermissionsBody = Json.createObjectBuilder().add("permissions", + Json.createObjectBuilder() + .add("isInheritanceEnabled", true) + .add("locallySet", Json.createObjectBuilder() + .add("authorityId", username) + .add("name", role) + .add("accessStatus", "DENIED"))) + .build().toString(); + restClient.authenticateUser(user).withCoreAPI().usingNode(file).updateNode(putPermissionsBody); + } +} diff --git a/remote-api/src/main/java/org/alfresco/rest/api/Categories.java b/remote-api/src/main/java/org/alfresco/rest/api/Categories.java index 695d3f64f1..578722d3f5 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/Categories.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/Categories.java @@ -54,7 +54,17 @@ public interface Categories void deleteCategoryById(String id, Parameters parameters); /** - * Link node to categories. Node types allowed for categorization are specified within {@link org.alfresco.util.TypeConstraint}. + * Get categories linked from node. Read permission on node is required. + * Node type is restricted to specified vales from: {@link org.alfresco.util.TypeConstraint}. + * + * @param nodeId Node ID. + * @return Categories linked from node. + */ + List listCategoriesForNode(String nodeId); + + /** + * Link node to categories. Change permission on node is required. + * Node types allowed for categorization are specified within {@link org.alfresco.util.TypeConstraint}. * * @param nodeId Node ID. * @param categoryLinks Category IDs to which content should be linked to. diff --git a/remote-api/src/main/java/org/alfresco/rest/api/categories/NodesCategoryLinksRelation.java b/remote-api/src/main/java/org/alfresco/rest/api/categories/NodesCategoryLinksRelation.java index 0eb7287e34..1a96b53e66 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/categories/NodesCategoryLinksRelation.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/categories/NodesCategoryLinksRelation.java @@ -35,10 +35,12 @@ import org.alfresco.rest.api.nodes.NodesEntityResource; import org.alfresco.rest.framework.WebApiDescription; import org.alfresco.rest.framework.resource.RelationshipResource; import org.alfresco.rest.framework.resource.actions.interfaces.RelationshipResourceAction; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.ListPage; import org.alfresco.rest.framework.resource.parameters.Parameters; @RelationshipResource(name = "category-links", entityResource = NodesEntityResource.class, title = "Category links") -public class NodesCategoryLinksRelation implements RelationshipResourceAction.Create +public class NodesCategoryLinksRelation implements RelationshipResourceAction.Read, RelationshipResourceAction.Create { private final Categories categories; @@ -48,12 +50,26 @@ public class NodesCategoryLinksRelation implements RelationshipResourceAction.Cr this.categories = categories; } + /** + * GET /nodes/{nodeId}/category-links + */ + @WebApiDescription( + title = "Get categories linked to by node", + description = "Get categories linked to by node", + successStatus = HttpServletResponse.SC_OK + ) + @Override + public CollectionWithPagingInfo readAll(String nodeId, Parameters parameters) + { + return ListPage.of(categories.listCategoriesForNode(nodeId), parameters.getPaging()); + } + /** * POST /nodes/{nodeId}/category-links */ @WebApiDescription( - title = "Link content node to categories", - description = "Creates a link between a content node and categories", + title = "Link node to categories", + description = "Creates a link between a node and categories", successStatus = HttpServletResponse.SC_CREATED ) @Override diff --git a/remote-api/src/main/java/org/alfresco/rest/api/impl/CategoriesImpl.java b/remote-api/src/main/java/org/alfresco/rest/api/impl/CategoriesImpl.java index d8c6364862..7c753a4b6a 100644 --- a/remote-api/src/main/java/org/alfresco/rest/api/impl/CategoriesImpl.java +++ b/remote-api/src/main/java/org/alfresco/rest/api/impl/CategoriesImpl.java @@ -32,6 +32,7 @@ import static org.alfresco.service.cmr.security.PermissionService.CHANGE_PERMISS import java.io.Serializable; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -71,6 +72,7 @@ public class CategoriesImpl implements Categories static final String NOT_A_VALID_CATEGORY = "Node id does not refer to a valid category"; static final String NO_PERMISSION_TO_MANAGE_A_CATEGORY = "Current user does not have permission to manage a category"; static final String NO_PERMISSION_TO_READ_CONTENT = "Current user does not have read permission to content"; + static final String NO_PERMISSION_TO_CHANGE_CONTENT = "Current user does not have change permission to content"; static final String NOT_NULL_OR_EMPTY = "Category name must not be null or empty"; static final String INVALID_NODE_TYPE = "Cannot categorize this node type"; @@ -158,6 +160,23 @@ public class CategoriesImpl implements Categories nodeService.deleteNode(nodeRef); } + @Override + public List listCategoriesForNode(final String nodeId) + { + final NodeRef contentNodeRef = nodes.validateNode(nodeId); + verifyReadPermission(contentNodeRef); + verifyNodeType(contentNodeRef); + + final Serializable currentCategories = nodeService.getProperty(contentNodeRef, ContentModel.PROP_CATEGORIES); + if (currentCategories == null) + { + return Collections.emptyList(); + } + final Collection actualCategories = DefaultTypeConverter.INSTANCE.getCollection(NodeRef.class, currentCategories); + + return actualCategories.stream().map(this::mapToCategory).collect(Collectors.toList()); + } + @Override public List linkNodeToCategories(final String nodeId, final List categoryLinks) { @@ -195,11 +214,19 @@ public class CategoriesImpl implements Categories } } + private void verifyReadPermission(final NodeRef nodeRef) + { + if (permissionService.hasReadPermission(nodeRef) != ALLOWED) + { + throw new PermissionDeniedException(NO_PERMISSION_TO_READ_CONTENT); + } + } + private void verifyChangePermission(final NodeRef nodeRef) { if (permissionService.hasPermission(nodeRef, CHANGE_PERMISSIONS) != ALLOWED) { - throw new PermissionDeniedException(NO_PERMISSION_TO_READ_CONTENT); + throw new PermissionDeniedException(NO_PERMISSION_TO_CHANGE_CONTENT); } } diff --git a/remote-api/src/main/java/org/alfresco/rest/framework/resource/SerializablePagedCollection.java b/remote-api/src/main/java/org/alfresco/rest/framework/resource/SerializablePagedCollection.java index 968b9c6172..946544ba48 100644 --- a/remote-api/src/main/java/org/alfresco/rest/framework/resource/SerializablePagedCollection.java +++ b/remote-api/src/main/java/org/alfresco/rest/framework/resource/SerializablePagedCollection.java @@ -51,7 +51,6 @@ public interface SerializablePagedCollection /** * Indicates the total number of items available. - * * Can be greater than the number of items returned in the list. * */ diff --git a/remote-api/src/main/java/org/alfresco/rest/framework/resource/parameters/ArrayListPage.java b/remote-api/src/main/java/org/alfresco/rest/framework/resource/parameters/ArrayListPage.java index 76e518186d..7e26717259 100644 --- a/remote-api/src/main/java/org/alfresco/rest/framework/resource/parameters/ArrayListPage.java +++ b/remote-api/src/main/java/org/alfresco/rest/framework/resource/parameters/ArrayListPage.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2022 Alfresco Software Limited + * Copyright (C) 2005 - 2023 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -27,13 +27,11 @@ package org.alfresco.rest.framework.resource.parameters; import org.alfresco.rest.api.search.context.SearchContext; -import org.alfresco.service.Experimental; import java.util.ArrayList; import java.util.Collections; import java.util.List; -@Experimental public class ArrayListPage extends ArrayList implements ListPage { private final Paging paging; diff --git a/remote-api/src/main/java/org/alfresco/rest/framework/resource/parameters/ListPage.java b/remote-api/src/main/java/org/alfresco/rest/framework/resource/parameters/ListPage.java index 8b0d99534c..7dd15e68c7 100644 --- a/remote-api/src/main/java/org/alfresco/rest/framework/resource/parameters/ListPage.java +++ b/remote-api/src/main/java/org/alfresco/rest/framework/resource/parameters/ListPage.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2022 Alfresco Software Limited + * Copyright (C) 2005 - 2023 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -28,7 +28,6 @@ package org.alfresco.rest.framework.resource.parameters; import org.alfresco.query.PagingResults; import org.alfresco.rest.framework.resource.SerializablePagedCollection; -import org.alfresco.service.Experimental; import org.alfresco.util.Pair; import java.util.Collection; @@ -38,10 +37,8 @@ import java.util.List; /** * List page with paging information. * - * * @param - list element type */ -@Experimental public interface ListPage extends List, PagingResults, SerializablePagedCollection { diff --git a/remote-api/src/test/java/org/alfresco/rest/api/categories/NodesCategoryLinksRelationTest.java b/remote-api/src/test/java/org/alfresco/rest/api/categories/NodesCategoryLinksRelationTest.java new file mode 100644 index 0000000000..6aa9b4afde --- /dev/null +++ b/remote-api/src/test/java/org/alfresco/rest/api/categories/NodesCategoryLinksRelationTest.java @@ -0,0 +1,91 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2023 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.api.categories; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; + +import java.util.List; + +import org.alfresco.rest.api.Categories; +import org.alfresco.rest.api.model.Category; +import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; +import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class NodesCategoryLinksRelationTest +{ + private static final String CONTENT_ID = "content-node-id"; + + @Mock + private Categories categoriesMock; + @Mock + private Category categoryMock; + @Mock + private Parameters parametersMock; + + @InjectMocks + private NodesCategoryLinksRelation objectUnderTest; + + @Test + public void testReadAll() + { + given(categoriesMock.listCategoriesForNode(any())).willReturn(List.of(categoryMock)); + + // when + final CollectionWithPagingInfo actualCategoriesPage = objectUnderTest.readAll(CONTENT_ID, parametersMock); + + then(categoriesMock).should().listCategoriesForNode(CONTENT_ID); + then(categoriesMock).shouldHaveNoMoreInteractions(); + assertThat(actualCategoriesPage) + .isNotNull() + .extracting(CollectionWithPagingInfo::getCollection) + .isEqualTo(List.of(categoryMock)); + } + + @Test + public void testCreate() + { + given(categoriesMock.linkNodeToCategories(any(), any())).willReturn(List.of(categoryMock)); + + // when + final List actualCategories = objectUnderTest.create(CONTENT_ID, List.of(categoryMock), parametersMock); + + then(categoriesMock).should().linkNodeToCategories(CONTENT_ID, List.of(categoryMock)); + then(categoriesMock).shouldHaveNoMoreInteractions(); + assertThat(actualCategories) + .isNotNull() + .isEqualTo(List.of(categoryMock)); + } +} \ No newline at end of file diff --git a/remote-api/src/test/java/org/alfresco/rest/api/impl/CategoriesImplTest.java b/remote-api/src/test/java/org/alfresco/rest/api/impl/CategoriesImplTest.java index f16a6b09d4..ab919462e2 100644 --- a/remote-api/src/test/java/org/alfresco/rest/api/impl/CategoriesImplTest.java +++ b/remote-api/src/test/java/org/alfresco/rest/api/impl/CategoriesImplTest.java @@ -30,6 +30,7 @@ import static org.alfresco.rest.api.Nodes.PATH_ROOT; import static org.alfresco.rest.api.impl.CategoriesImpl.INVALID_NODE_TYPE; import static org.alfresco.rest.api.impl.CategoriesImpl.NOT_A_VALID_CATEGORY; import static org.alfresco.rest.api.impl.CategoriesImpl.NOT_NULL_OR_EMPTY; +import static org.alfresco.rest.api.impl.CategoriesImpl.NO_PERMISSION_TO_CHANGE_CONTENT; import static org.alfresco.rest.api.impl.CategoriesImpl.NO_PERMISSION_TO_READ_CONTENT; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -39,7 +40,6 @@ import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; @@ -54,6 +54,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; +import java.util.stream.Stream; import org.alfresco.model.ContentModel; import org.alfresco.rest.api.Nodes; @@ -127,6 +128,7 @@ public class CategoriesImplTest given(nodesMock.validateNode(CONTENT_NODE_ID)).willReturn(CONTENT_NODE_REF); given(nodesMock.isSubClass(any(), any(), anyBoolean())).willReturn(true); given(typeConstraint.matches(any())).willReturn(true); + given(permissionServiceMock.hasReadPermission(any())).willReturn(AccessStatus.ALLOWED); given(permissionServiceMock.hasPermission(any(), any())).willReturn(AccessStatus.ALLOWED); } @@ -944,7 +946,7 @@ public class CategoriesImplTest then(nodeServiceMock).shouldHaveNoInteractions(); assertThat(actualException) .isInstanceOf(PermissionDeniedException.class) - .hasMessageContaining(NO_PERMISSION_TO_READ_CONTENT); + .hasMessageContaining(NO_PERMISSION_TO_CHANGE_CONTENT); } @Test @@ -998,6 +1000,94 @@ public class CategoriesImplTest .hasMessageContaining(NOT_A_VALID_CATEGORY); } + @Test + public void testListCategoriesForNode() + { + final NodeRef categoryParentNodeRef = createNodeRefWithId(PARENT_ID); + final ChildAssociationRef parentAssociation = createAssociationOf(categoryParentNodeRef, CATEGORY_NODE_REF); + given(nodeServiceMock.getProperty(any(), eq(ContentModel.PROP_CATEGORIES))).willReturn((Serializable) List.of(CATEGORY_NODE_REF)); + given(nodesMock.getNode(any())).willReturn(prepareCategoryNode()); + given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation); + + // when + final List actualCategories = objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID); + + then(nodesMock).should().validateNode(CONTENT_NODE_ID); + then(permissionServiceMock).should().hasReadPermission(CONTENT_NODE_REF); + then(permissionServiceMock).shouldHaveNoMoreInteractions(); + then(typeConstraint).should().matches(CONTENT_NODE_REF); + then(typeConstraint).shouldHaveNoMoreInteractions(); + then(nodesMock).should().getNode(CATEGORY_ID); + then(nodesMock).shouldHaveNoMoreInteractions(); + then(nodeServiceMock).should().getProperty(CONTENT_NODE_REF, ContentModel.PROP_CATEGORIES); + then(nodeServiceMock).should().getChildAssocs(CATEGORY_NODE_REF, RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false); + then(nodeServiceMock).should().getPrimaryParent(CATEGORY_NODE_REF); + then(nodeServiceMock).should().getParentAssocs(categoryParentNodeRef); + then(nodeServiceMock).shouldHaveNoMoreInteractions(); + final List expectedCategories = List.of(CATEGORY); + assertThat(actualCategories) + .isNotNull().usingRecursiveComparison() + .isEqualTo(expectedCategories); + } + + @Test + public void testListCategoriesForNode_withInvalidNodeId() + { + given(nodesMock.validateNode(CONTENT_NODE_ID)).willThrow(EntityNotFoundException.class); + + // when + final Throwable actualException = catchThrowable(() -> objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID)); + + then(nodesMock).should().validateNode(CONTENT_NODE_ID); + then(nodeServiceMock).shouldHaveNoInteractions(); + assertThat(actualException) + .isInstanceOf(EntityNotFoundException.class); + } + + @Test + public void testListCategoriesForNode_withoutPermission() + { + given(permissionServiceMock.hasReadPermission(any())).willReturn(AccessStatus.DENIED); + + // when + final Throwable actualException = catchThrowable(() -> objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID)); + + then(nodesMock).should().validateNode(CONTENT_NODE_ID); + then(permissionServiceMock).should().hasReadPermission(CONTENT_NODE_REF); + then(nodeServiceMock).shouldHaveNoInteractions(); + assertThat(actualException) + .isInstanceOf(PermissionDeniedException.class) + .hasMessageContaining(NO_PERMISSION_TO_READ_CONTENT); + } + + @Test + public void testListCategoriesForNode_withInvalidNodeType() + { + given(typeConstraint.matches(any())).willReturn(false); + + // when + final Throwable actualException = catchThrowable(() -> objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID)); + + then(typeConstraint).should().matches(CONTENT_NODE_REF); + then(nodeServiceMock).shouldHaveNoInteractions(); + assertThat(actualException) + .isInstanceOf(UnsupportedResourceOperationException.class) + .hasMessageContaining(INVALID_NODE_TYPE); + } + + @Test + public void testListCategoriesForNode_withoutLinkedCategories() + { + Stream.of(null, Collections.emptyList()).forEach(nullOrEmptyList -> { + given(nodeServiceMock.getProperty(any(), eq(ContentModel.PROP_CATEGORIES))).willReturn((Serializable) nullOrEmptyList); + + // when + final List actualCategories = objectUnderTest.listCategoriesForNode(CONTENT_NODE_ID); + + assertThat(actualCategories).isNotNull().isEmpty(); + }); + } + private Node prepareCategoryNode(final String name, final String id, final NodeRef parentNodeRef) { final Node categoryNode = new Node(); diff --git a/remote-api/src/test/java/org/alfresco/rest/framework/resource/parameters/ArrayListPageTest.java b/remote-api/src/test/java/org/alfresco/rest/framework/resource/parameters/ArrayListPageTest.java index df9a88dad2..92f3f87520 100644 --- a/remote-api/src/test/java/org/alfresco/rest/framework/resource/parameters/ArrayListPageTest.java +++ b/remote-api/src/test/java/org/alfresco/rest/framework/resource/parameters/ArrayListPageTest.java @@ -2,7 +2,7 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2022 Alfresco Software Limited + * Copyright (C) 2005 - 2023 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -28,7 +28,6 @@ package org.alfresco.rest.framework.resource.parameters; import junit.framework.TestCase; import org.alfresco.rest.framework.resource.SerializablePagedCollection; -import org.alfresco.service.Experimental; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @@ -42,7 +41,6 @@ import java.util.Random; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -@Experimental @RunWith(MockitoJUnitRunner.class) public class ArrayListPageTest extends TestCase {