ACS-4034: Assign a piece of content to a category (#1655)

* ACS-4034: Assign a piece of content to a category - POST /nodes/{nodeId}/category-links
- added also one TAS test for update category covering repeated name case
This commit is contained in:
krdabrowski
2023-01-09 16:01:57 +01:00
committed by GitHub
parent 6a240b722b
commit e4e4ed214a
10 changed files with 847 additions and 21 deletions

View File

@@ -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
@@ -53,4 +53,12 @@ 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}.
*
* @param nodeId Node ID.
* @param categoryLinks Category IDs to which content should be linked to.
* @return Linked to categories.
*/
List<Category> linkNodeToCategories(String nodeId, List<Category> categoryLinks);
}

View File

@@ -0,0 +1,64 @@
/*
* #%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 <http://www.gnu.org/licenses/>.
* #L%
*/
package org.alfresco.rest.api.categories;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import org.alfresco.rest.api.Categories;
import org.alfresco.rest.api.model.Category;
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.Parameters;
@RelationshipResource(name = "category-links", entityResource = NodesEntityResource.class, title = "Category links")
public class NodesCategoryLinksRelation implements RelationshipResourceAction.Create<Category>
{
private final Categories categories;
public NodesCategoryLinksRelation(Categories categories)
{
this.categories = categories;
}
/**
* POST /nodes/{nodeId}/category-links
*/
@WebApiDescription(
title = "Link content node to categories",
description = "Creates a link between a content node and categories",
successStatus = HttpServletResponse.SC_CREATED
)
@Override
public List<Category> create(String nodeId, List<Category> categoryLinks, Parameters parameters)
{
return categories.linkNodeToCategories(nodeId, categoryLinks);
}
}

View File

@@ -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,8 +27,15 @@
package org.alfresco.rest.api.impl;
import static org.alfresco.rest.api.Nodes.PATH_ROOT;
import static org.alfresco.service.cmr.security.AccessStatus.ALLOWED;
import static org.alfresco.service.cmr.security.PermissionService.CHANGE_PERMISSIONS;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import org.alfresco.model.ContentModel;
@@ -39,6 +46,7 @@ import org.alfresco.rest.api.model.Node;
import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.ListPage;
import org.alfresco.rest.framework.resource.parameters.Parameters;
@@ -47,10 +55,13 @@ import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
import org.alfresco.service.cmr.search.CategoryService;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.util.TypeConstraint;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
@@ -59,20 +70,26 @@ 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 NOT_NULL_OR_EMPTY = "Category name must not be null or empty";
static final String FIELD_NOT_MATCH = "Category field: %s does not match the original one";
static final String INVALID_NODE_TYPE = "Cannot categorize this node type";
private final AuthorityService authorityService;
private final CategoryService categoryService;
private final Nodes nodes;
private final NodeService nodeService;
private final PermissionService permissionService;
private final TypeConstraint typeConstraint;
public CategoriesImpl(AuthorityService authorityService, CategoryService categoryService, Nodes nodes, NodeService nodeService)
public CategoriesImpl(AuthorityService authorityService, CategoryService categoryService, Nodes nodes, NodeService nodeService, PermissionService permissionService,
TypeConstraint typeConstraint)
{
this.authorityService = authorityService;
this.categoryService = categoryService;
this.nodes = nodes;
this.nodeService = nodeService;
this.permissionService = permissionService;
this.typeConstraint = typeConstraint;
}
@Override
@@ -123,7 +140,7 @@ public class CategoriesImpl implements Categories
throw new InvalidArgumentException(NOT_A_VALID_CATEGORY, new String[]{id});
}
verifyCategoryFields(fixedCategoryModel);
validateCategoryFields(fixedCategoryModel);
return mapToCategory(changeCategoryName(categoryNodeRef, fixedCategoryModel.getName()));
}
@@ -141,6 +158,35 @@ public class CategoriesImpl implements Categories
nodeService.deleteNode(nodeRef);
}
@Override
public List<Category> linkNodeToCategories(final String nodeId, final List<Category> categoryLinks)
{
if (CollectionUtils.isEmpty(categoryLinks))
{
throw new InvalidArgumentException(NOT_A_VALID_CATEGORY);
}
final NodeRef contentNodeRef = nodes.validateNode(nodeId);
verifyChangePermission(contentNodeRef);
verifyNodeType(contentNodeRef);
final Collection<NodeRef> categoryNodeRefs = categoryLinks.stream()
.filter(Objects::nonNull)
.map(Category::getId)
.filter(StringUtils::isNotEmpty)
.map(this::getCategoryNodeRef)
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(categoryNodeRefs) || isRootCategoryPresent(categoryNodeRefs))
{
throw new InvalidArgumentException(NOT_A_VALID_CATEGORY);
}
linkNodeToCategories(contentNodeRef, categoryNodeRefs);
return categoryNodeRefs.stream().map(this::mapToCategory).collect(Collectors.toList());
}
private void verifyAdminAuthority()
{
if (!authorityService.hasAdminAuthority())
@@ -149,6 +195,22 @@ public class CategoriesImpl implements Categories
}
}
private void verifyChangePermission(final NodeRef nodeRef)
{
if (permissionService.hasPermission(nodeRef, CHANGE_PERMISSIONS) != ALLOWED)
{
throw new PermissionDeniedException(NO_PERMISSION_TO_READ_CONTENT);
}
}
private void verifyNodeType(final NodeRef nodeRef)
{
if (!typeConstraint.matches(nodeRef))
{
throw new UnsupportedResourceOperationException(INVALID_NODE_TYPE);
}
}
/**
* This method gets category NodeRef for a given category id.
* If '-root-' is passed as category id, then it's retrieved as a call to {@link org.alfresco.service.cmr.search.CategoryService#getRootCategoryNodeRef}
@@ -181,7 +243,7 @@ public class CategoriesImpl implements Categories
private NodeRef createCategoryNodeRef(NodeRef parentNodeRef, Category c)
{
verifyCategoryFields(c);
validateCategoryFields(c);
return categoryService.createCategory(parentNodeRef, c.getName());
}
@@ -236,15 +298,67 @@ public class CategoriesImpl implements Categories
}
/**
* Verify if fixed category name is not empty.
* Validate if fixed category name is not empty.
*
* @param fixedCategoryModel Fixed category model.
*/
private void verifyCategoryFields(final Category fixedCategoryModel)
private void validateCategoryFields(final Category fixedCategoryModel)
{
if (StringUtils.isEmpty(fixedCategoryModel.getName()))
{
throw new InvalidArgumentException(NOT_NULL_OR_EMPTY);
}
}
private boolean isRootCategoryPresent(final Collection<NodeRef> categoryNodeRefs)
{
return categoryNodeRefs.stream().anyMatch(this::isRootCategory);
}
private boolean isCategoryAspectMissing(final NodeRef nodeRef)
{
return !nodeService.hasAspect(nodeRef, ContentModel.ASPECT_GEN_CLASSIFIABLE);
}
/**
* Merge already present and new categories ignoring repeating ones.
*
* @param currentCategories Already present categories.
* @param newCategories Categories which should be added.
* @return Merged categories.
*/
private Collection<NodeRef> mergeCategories(final Serializable currentCategories, final Collection<NodeRef> newCategories)
{
if (currentCategories == null)
{
return newCategories;
}
final Collection<NodeRef> actualCategories = DefaultTypeConverter.INSTANCE.getCollection(NodeRef.class, currentCategories);
final Collection<NodeRef> allCategories = new HashSet<>(actualCategories);
allCategories.addAll(newCategories);
return allCategories;
}
/**
* Add to or update node's property cm:categories containing linked category references.
*
* @param nodeRef Node reference.
* @param categoryNodeRefs Category node references.
*/
private void linkNodeToCategories(final NodeRef nodeRef, final Collection<NodeRef> categoryNodeRefs)
{
if (isCategoryAspectMissing(nodeRef))
{
final Map<QName, Serializable> properties = Map.of(ContentModel.PROP_CATEGORIES, (Serializable) categoryNodeRefs);
nodeService.addAspect(nodeRef, ContentModel.ASPECT_GEN_CLASSIFIABLE, properties);
}
else
{
final Serializable currentCategories = nodeService.getProperty(nodeRef, ContentModel.PROP_CATEGORIES);
final Collection<NodeRef> allCategories = mergeCategories(currentCategories, categoryNodeRefs);
nodeService.setProperty(nodeRef, ContentModel.PROP_CATEGORIES, (Serializable) allCategories);
}
}
}

View File

@@ -45,6 +45,11 @@ public class Category
this.id = id;
}
public void setCategoryId(String categoryId)
{
this.id = categoryId;
}
public String getName()
{
return name;

View File

@@ -834,6 +834,8 @@
<constructor-arg name="categoryService" ref="CategoryService"/>
<constructor-arg name="nodes" ref="nodes"/>
<constructor-arg name="nodeService" ref="NodeService"/>
<constructor-arg name="permissionService" ref="PermissionService"/>
<constructor-arg name="typeConstraint" ref="nodeTypeConstraint"/>
</bean>
<bean id="Categories" class="org.springframework.aop.framework.ProxyFactoryBean">
@@ -1105,6 +1107,10 @@
<constructor-arg name="categories" ref="Categories"/>
</bean>
<bean class="org.alfresco.rest.api.categories.NodesCategoryLinksRelation">
<constructor-arg name="categories" ref="Categories"/>
</bean>
<bean class="org.alfresco.rest.api.tags.TagsEntityResource">
<property name="tags" ref="Tags" />
</bean>

View File

@@ -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,24 +27,31 @@
package org.alfresco.rest.api.impl;
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_READ_CONTENT;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.junit.Assert.assertEquals;
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;
import static org.mockito.Mockito.times;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -55,6 +62,7 @@ import org.alfresco.rest.api.model.Node;
import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException;
import org.alfresco.rest.framework.core.exceptions.UnsupportedResourceOperationException;
import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
@@ -62,9 +70,14 @@ import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.search.CategoryService;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.util.TypeConstraint;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -81,6 +94,8 @@ public class CategoriesImplTest
private static final String CAT_ROOT_NODE_ID = "cat-root-node-id";
private static final NodeRef CATEGORY_NODE_REF = createNodeRefWithId(CATEGORY_ID);
private static final Category CATEGORY = createDefaultCategoryWithName(CATEGORY_NAME);
private static final String CONTENT_NODE_ID = "content-node-id";
private static final NodeRef CONTENT_NODE_REF = createNodeRefWithId(CONTENT_NODE_ID);
@Mock
private Nodes nodesMock;
@@ -96,6 +111,10 @@ public class CategoriesImplTest
private ChildAssociationRef dummyChildAssociationRefMock;
@Mock
private ChildAssociationRef categoryChildAssociationRefMock;
@Mock
private PermissionService permissionServiceMock;
@Mock
private TypeConstraint typeConstraint;
@InjectMocks
private CategoriesImpl objectUnderTest;
@@ -104,8 +123,11 @@ public class CategoriesImplTest
public void setUp() throws Exception
{
given(authorityServiceMock.hasAdminAuthority()).willReturn(true);
given(nodesMock.validateNode(eq(CATEGORY_ID))).willReturn(CATEGORY_NODE_REF);
given(nodesMock.validateNode(CATEGORY_ID)).willReturn(CATEGORY_NODE_REF);
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.hasPermission(any(), any())).willReturn(AccessStatus.ALLOWED);
}
@Test
@@ -774,6 +796,208 @@ public class CategoriesImplTest
.isEqualTo(expectedCategory);
}
@Test
public void testLinkNodeToCategories_withoutCategoryAspect()
{
final List<Category> categoryLinks = List.of(CATEGORY);
final NodeRef categoryParentNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef parentAssociation = createAssociationOf(categoryParentNodeRef, CATEGORY_NODE_REF);
given(nodesMock.getNode(any())).willReturn(prepareCategoryNode());
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
// when
final List<Category> actualLinkedCategories = objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, categoryLinks);
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(permissionServiceMock).should().hasPermission(CONTENT_NODE_REF, PermissionService.CHANGE_PERMISSIONS);
then(permissionServiceMock).shouldHaveNoMoreInteractions();
then(typeConstraint).should().matches(CONTENT_NODE_REF);
then(typeConstraint).shouldHaveNoMoreInteractions();
then(nodesMock).should().validateNode(CATEGORY_ID);
then(nodesMock).should().getNode(CATEGORY_ID);
then(nodesMock).should().isSubClass(CATEGORY_NODE_REF, ContentModel.TYPE_CATEGORY, false);
then(nodesMock).shouldHaveNoMoreInteractions();
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(CATEGORY_NODE_REF);
then(nodeServiceMock).should().hasAspect(CONTENT_NODE_REF, ContentModel.ASPECT_GEN_CLASSIFIABLE);
final Map<QName, Serializable> expectedProperties = Map.of(ContentModel.PROP_CATEGORIES, (Serializable) List.of(CATEGORY_NODE_REF));
then(nodeServiceMock).should().addAspect(CONTENT_NODE_REF, ContentModel.ASPECT_GEN_CLASSIFIABLE, expectedProperties);
then(nodeServiceMock).should().getParentAssocs(categoryParentNodeRef);
then(nodeServiceMock).shouldHaveNoMoreInteractions();
final List<Category> expectedLinkedCategories = List.of(CATEGORY);
assertThat(actualLinkedCategories)
.isNotNull().usingRecursiveComparison()
.isEqualTo(expectedLinkedCategories);
}
@Test
public void testLinkNodeToCategories_withPresentCategoryAspect()
{
final NodeRef categoryParentNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef parentAssociation = createAssociationOf(categoryParentNodeRef, CATEGORY_NODE_REF);
given(nodesMock.getNode(any())).willReturn(prepareCategoryNode());
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
given(nodeServiceMock.hasAspect(any(), any())).willReturn(true);
// when
final List<Category> actualLinkedCategories = objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY));
then(nodesMock).should().getNode(CATEGORY_ID);
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(CATEGORY_NODE_REF);
then(nodeServiceMock).should().hasAspect(CONTENT_NODE_REF, ContentModel.ASPECT_GEN_CLASSIFIABLE);
then(nodeServiceMock).should().getProperty(CONTENT_NODE_REF, ContentModel.PROP_CATEGORIES);
final Serializable expectedCategories = (Serializable) List.of(CATEGORY_NODE_REF);
then(nodeServiceMock).should().setProperty(CONTENT_NODE_REF, ContentModel.PROP_CATEGORIES, expectedCategories);
then(nodeServiceMock).should().getParentAssocs(categoryParentNodeRef);
then(nodeServiceMock).shouldHaveNoMoreInteractions();
final List<Category> expectedLinkedCategories = List.of(CATEGORY);
assertThat(actualLinkedCategories)
.isNotNull().usingRecursiveComparison()
.isEqualTo(expectedLinkedCategories);
}
@Test
public void testLinkNodeToCategories_withMultipleCategoryIds()
{
final String secondCategoryId = "second-category-id";
final String secondCategoryName = "secondCategoryName";
final NodeRef secondCategoryNodeRef = createNodeRefWithId(secondCategoryId);
final Category secondCategoryLink = Category.builder().id(secondCategoryId).create();
final List<Category> categoryLinks = List.of(CATEGORY, secondCategoryLink);
final NodeRef categoryParentNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef categoryParentAssociation = createAssociationOf(categoryParentNodeRef, CATEGORY_NODE_REF);
final ChildAssociationRef secondCategoryParentAssociation = createAssociationOf(categoryParentNodeRef, secondCategoryNodeRef);
given(nodesMock.validateNode(secondCategoryId)).willReturn(secondCategoryNodeRef);
given(nodesMock.getNode(any())).willReturn(prepareCategoryNode(), prepareCategoryNode(secondCategoryName));
given(nodeServiceMock.getPrimaryParent(any())).willReturn(categoryParentAssociation, secondCategoryParentAssociation);
// when
final List<Category> actualLinkedCategories = objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, categoryLinks);
then(nodesMock).should().validateNode(CATEGORY_ID);
then(nodesMock).should().validateNode(secondCategoryId);
then(nodesMock).should().isSubClass(CATEGORY_NODE_REF, ContentModel.TYPE_CATEGORY, false);
then(nodesMock).should().isSubClass(secondCategoryNodeRef, ContentModel.TYPE_CATEGORY, false);
final Map<QName, Serializable> expectedProperties = Map.of(ContentModel.PROP_CATEGORIES, (Serializable) List.of(CATEGORY_NODE_REF, secondCategoryNodeRef));
then(nodeServiceMock).should().addAspect(CONTENT_NODE_REF, ContentModel.ASPECT_GEN_CLASSIFIABLE, expectedProperties);
final List<Category> expectedLinkedCategories = List.of(
CATEGORY,
Category.builder().id(secondCategoryId).name(secondCategoryName).parentId(PARENT_ID).create()
);
assertThat(actualLinkedCategories)
.isNotNull().usingRecursiveComparison()
.isEqualTo(expectedLinkedCategories);
}
@Test
public void testLinkNodeToCategories_withPreviouslyLinkedCategories()
{
final String otherCategoryId = "other-category-id";
final NodeRef otherCategoryNodeRef = createNodeRefWithId(otherCategoryId);
final Serializable previousCategories = (Serializable) List.of(otherCategoryNodeRef);
final NodeRef categoryParentNodeRef = createNodeRefWithId(PARENT_ID);
final ChildAssociationRef parentAssociation = createAssociationOf(categoryParentNodeRef, CATEGORY_NODE_REF);
given(nodesMock.getNode(any())).willReturn(prepareCategoryNode());
given(nodeServiceMock.getPrimaryParent(any())).willReturn(parentAssociation);
given(nodeServiceMock.hasAspect(any(), any())).willReturn(true);
given(nodeServiceMock.getProperty(any(), eq(ContentModel.PROP_CATEGORIES))).willReturn(previousCategories);
// when
final List<Category> actualLinkedCategories = objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY));
final Serializable expectedCategories = (Serializable) Set.of(otherCategoryNodeRef, CATEGORY_NODE_REF);
then(nodeServiceMock).should().setProperty(CONTENT_NODE_REF, ContentModel.PROP_CATEGORIES, expectedCategories);
final List<Category> expectedLinkedCategories = List.of(CATEGORY);
assertThat(actualLinkedCategories)
.isNotNull().usingRecursiveComparison()
.isEqualTo(expectedLinkedCategories);
}
@Test
public void testLinkNodeToCategories_withInvalidNodeId()
{
given(nodesMock.validateNode(CONTENT_NODE_ID)).willThrow(EntityNotFoundException.class);
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY)));
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(permissionServiceMock).shouldHaveNoInteractions();
then(nodeServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(EntityNotFoundException.class);
}
@Test
public void testLinkNodeToCategories_withoutPermission()
{
given(permissionServiceMock.hasPermission(any(), any())).willReturn(AccessStatus.DENIED);
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY)));
then(nodesMock).should().validateNode(CONTENT_NODE_ID);
then(permissionServiceMock).should().hasPermission(CONTENT_NODE_REF, PermissionService.CHANGE_PERMISSIONS);
then(nodeServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(PermissionDeniedException.class)
.hasMessageContaining(NO_PERMISSION_TO_READ_CONTENT);
}
@Test
public void testLinkContentNodeToCategories_withInvalidNodeType()
{
given(typeConstraint.matches(any())).willReturn(false);
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, List.of(CATEGORY)));
then(typeConstraint).should().matches(CONTENT_NODE_REF);
then(nodeServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(UnsupportedResourceOperationException.class)
.hasMessageContaining(INVALID_NODE_TYPE);
}
@Test
public void testLinkNodeToCategories_withEmptyLinks()
{
final List<Category> categoryLinks = Collections.emptyList();
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, categoryLinks));
then(nodesMock).shouldHaveNoInteractions();
then(permissionServiceMock).shouldHaveNoInteractions();
then(nodeServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(InvalidArgumentException.class)
.hasMessageContaining(NOT_A_VALID_CATEGORY);
}
@Test
public void testLinkNodeToCategories_withInvalidCategoryIds()
{
final Category categoryLinkWithNullId = Category.builder().id(null).create();
final Category categoryLinkWithEmptyId = Category.builder().id(StringUtils.EMPTY).create();
final List<Category> categoryLinks = new ArrayList<>();
categoryLinks.add(categoryLinkWithNullId);
categoryLinks.add(null);
categoryLinks.add(categoryLinkWithEmptyId);
// when
final Throwable actualException = catchThrowable(() -> objectUnderTest.linkNodeToCategories(CONTENT_NODE_ID, categoryLinks));
then(nodeServiceMock).shouldHaveNoInteractions();
assertThat(actualException)
.isInstanceOf(InvalidArgumentException.class)
.hasMessageContaining(NOT_A_VALID_CATEGORY);
}
private Node prepareCategoryNode(final String name, final String id, final NodeRef parentNodeRef)
{
final Node categoryNode = new Node();
@@ -785,8 +1009,7 @@ public class CategoriesImplTest
private Node prepareCategoryNode(final String name)
{
final NodeRef parentNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_ID);
return prepareCategoryNode(name, CATEGORY_ID, parentNodeRef);
return prepareCategoryNode(name, CATEGORY_ID, createNodeRefWithId(PARENT_ID));
}
private Node prepareCategoryNode()
@@ -858,7 +1081,12 @@ public class CategoriesImplTest
private static QName createCmQNameOf(final String name)
{
return QName.createQName(ContentModel.TYPE_CATEGORY.getNamespaceURI(), QName.createValidLocalName(name));
return QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, QName.createValidLocalName(name));
}
private static ChildAssociationRef createAssociationOf(final NodeRef parentNode, final NodeRef childNode)
{
return createAssociationOf(parentNode, childNode, null);
}
private static ChildAssociationRef createAssociationOf(final NodeRef parentNode, final NodeRef childNode, final QName childNodeName)