ACS-4031 list category children (#1617)

* ACS-4031: Get category children endpoint.

* ACS-4031: Fix in E2E test.

* ACS-4031: Adding more unit tests.

* Fix test description

Co-authored-by: Tom Page <tpage-alfresco@users.noreply.github.com>

* Fix test description

Co-authored-by: Tom Page <tpage-alfresco@users.noreply.github.com>

* ACS-4031: Refactoring after code review.

* ACS-4031: Updating E2E test after latest logic refactor.

Co-authored-by: Tom Page <tpage-alfresco@users.noreply.github.com>
This commit is contained in:
Maciej Pichura
2022-12-13 17:13:02 +01:00
committed by GitHub
parent c1198a0145
commit 0aae95b255
8 changed files with 501 additions and 29 deletions

View File

@@ -29,6 +29,7 @@ package org.alfresco.rest.api;
import java.util.List;
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.alfresco.service.Experimental;
import org.alfresco.service.cmr.repository.NodeRef;
@@ -39,4 +40,6 @@ public interface Categories
Category getCategoryById(String id, Parameters params);
List<Category> createSubcategories(String parentCategoryId, List<Category> categories, Parameters parameters);
CollectionWithPagingInfo<Category> getCategoryChildren(String parentCategoryId, Parameters params);
}

View File

@@ -35,10 +35,11 @@ import org.alfresco.rest.api.model.Category;
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.Parameters;
@RelationshipResource(name = "subcategories", entityResource = CategoriesEntityResource.class, title = "Subcategories")
public class SubcategoriesRelation implements RelationshipResourceAction.Create<Category>
public class SubcategoriesRelation implements RelationshipResourceAction.Create<Category>, RelationshipResourceAction.Read<Category>
{
private final Categories categories;
@@ -56,4 +57,13 @@ public class SubcategoriesRelation implements RelationshipResourceAction.Create<
{
return categories.createSubcategories(parentCategoryId, categoryList, parameters);
}
@WebApiDescription(title = "List category direct children",
description = "Lists direct children of a parent category",
successStatus = HttpServletResponse.SC_OK)
@Override
public CollectionWithPagingInfo<Category> readAll(String parentCategoryId, Parameters params)
{
return categories.getCategoryChildren(parentCategoryId, params);
}
}

View File

@@ -39,6 +39,8 @@ 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.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.ListPage;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.Experimental;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
@@ -74,8 +76,8 @@ public class CategoriesImpl implements Categories
@Override
public Category getCategoryById(final String id, final Parameters params)
{
final NodeRef nodeRef = nodes.validateNode(id);
if (isNotACategory(nodeRef) || isRootCategory(nodeRef))
final NodeRef nodeRef = getCategoryNodeRef(id);
if (isRootCategory(nodeRef))
{
throw new InvalidArgumentException(NOT_A_VALID_CATEGORY, new String[]{id});
}
@@ -90,14 +92,7 @@ public class CategoriesImpl implements Categories
{
throw new PermissionDeniedException(NO_PERMISSION_TO_CREATE_A_CATEGORY);
}
final NodeRef parentNodeRef = PATH_ROOT.equals(parentCategoryId) ?
categoryService.getRootCategoryNodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE)
.orElseThrow(() -> new EntityNotFoundException(parentCategoryId)) :
nodes.validateNode(parentCategoryId);
if (isNotACategory(parentNodeRef))
{
throw new InvalidArgumentException(NOT_A_VALID_CATEGORY, new String[]{parentCategoryId});
}
final NodeRef parentNodeRef = getCategoryNodeRef(parentCategoryId);
final List<NodeRef> categoryNodeRefs = categories.stream()
.map(c -> createCategoryNodeRef(parentNodeRef, c))
.collect(Collectors.toList());
@@ -106,6 +101,49 @@ public class CategoriesImpl implements Categories
.collect(Collectors.toList());
}
@Override
public CollectionWithPagingInfo<Category> getCategoryChildren(String parentCategoryId, Parameters params)
{
final NodeRef parentNodeRef = getCategoryNodeRef(parentCategoryId);
final List<ChildAssociationRef> childCategoriesAssocs = nodeService.getChildAssocs(parentNodeRef).stream()
.filter(ca -> ContentModel.ASSOC_SUBCATEGORIES.equals(ca.getTypeQName()))
.collect(Collectors.toList());
final List<Category> categories = childCategoriesAssocs.stream()
.map(c -> mapToCategory(c.getChildRef()))
.collect(Collectors.toList());
return ListPage.of(categories, params.getPaging());
}
/**
* 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}
* In all other cases it's retrieved as a node of a category type {@link #validateCategoryNode(String)}
* @param nodeId category node id
* @return NodRef of category node
*/
private NodeRef getCategoryNodeRef(String nodeId)
{
return PATH_ROOT.equals(nodeId) ?
categoryService.getRootCategoryNodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE)
.orElseThrow(() -> new EntityNotFoundException(nodeId)) :
validateCategoryNode(nodeId);
}
/**
* Validates if the node exists and is a category.
* @param nodeId (presumably) category node id
* @return category NodeRef
*/
private NodeRef validateCategoryNode(String nodeId)
{
final NodeRef nodeRef = nodes.validateNode(nodeId);
if (isNotACategory(nodeRef))
{
throw new InvalidArgumentException(NOT_A_VALID_CATEGORY, new String[]{nodeId});
}
return nodeRef;
}
private NodeRef createCategoryNodeRef(NodeRef parentNodeRef, Category c)
{
if (StringUtils.isEmpty(c.getName())) {

View File

@@ -0,0 +1,105 @@
/*
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2022 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 static org.junit.Assert.assertEquals;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
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.Paging;
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 SubcategoriesRelationTest
{
private static final String PARENT_CATEGORY_ID = "parent-category-node-id";
private static final String CATEGORY_ID = "category-node-id";
private static final String CATEGORY_NAME = "categoryName";
private static final String SUBCATEGORY_NAME_PREFIX = "childCategoryName";
@Mock
private Categories categoriesMock;
@Mock
private Parameters parametersMock;
@InjectMocks
private SubcategoriesRelation objectUnderTest;
@Test
public void testCreateSubcategory()
{
final Category categoryToCreate = Category.builder().name(CATEGORY_NAME).create();
final Category category = Category.builder().name(CATEGORY_NAME).parentId(PARENT_CATEGORY_ID).hasChildren(false).id(CATEGORY_ID).create();
final List<Category> categoriesToCreate = List.of(categoryToCreate);
given(categoriesMock.createSubcategories(PARENT_CATEGORY_ID, categoriesToCreate, parametersMock)).willReturn(List.of(category));
//when
List<Category> categories = objectUnderTest.create(PARENT_CATEGORY_ID, categoriesToCreate, parametersMock);
then(categoriesMock).should().createSubcategories(PARENT_CATEGORY_ID, categoriesToCreate, parametersMock);
then(categoriesMock).shouldHaveNoMoreInteractions();
assertEquals(List.of(category), categories);
}
@Test
public void testGetCategoryChildren() {
final CollectionWithPagingInfo<Category> categoryChildren = getCategories(3);
given(categoriesMock.getCategoryChildren(PARENT_CATEGORY_ID, parametersMock)).willReturn(categoryChildren);
//when
final CollectionWithPagingInfo<Category> returnedChildren = objectUnderTest.readAll(PARENT_CATEGORY_ID, parametersMock);
then(categoriesMock).should().getCategoryChildren(PARENT_CATEGORY_ID, parametersMock);
then(categoriesMock).shouldHaveNoMoreInteractions();
assertEquals(categoryChildren, returnedChildren);
}
private CollectionWithPagingInfo<Category> getCategories(final int count)
{
return CollectionWithPagingInfo.asPaged(Paging.DEFAULT,
IntStream.range(0, count)
.mapToObj(i -> Category.builder().name(SUBCATEGORY_NAME_PREFIX + "-" + i)
.parentId(PARENT_CATEGORY_ID)
.hasChildren(false)
.id(CATEGORY_ID + "-" + i)
.create())
.collect(Collectors.toList())
);
}
}

View File

@@ -31,10 +31,15 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
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.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.alfresco.model.ContentModel;
import org.alfresco.rest.api.Nodes;
@@ -43,6 +48,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.resource.parameters.CollectionWithPagingInfo;
import org.alfresco.rest.framework.resource.parameters.Parameters;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
@@ -231,7 +237,6 @@ public class CategoriesImplTest
final NodeRef parentCategoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PATH_ROOT);
given(categoryServiceMock.getRootCategoryNodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE))
.willReturn(Optional.of(parentCategoryNodeRef));
given(nodesMock.isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false)).willReturn(true);
final NodeRef categoryNodeRef = prepareCategoryNodeRef();
given(categoryServiceMock.createCategory(parentCategoryNodeRef, CATEGORY_NAME)).willReturn(categoryNodeRef);
given(nodesMock.getNode(CATEGORY_ID)).willReturn(prepareCategoryNode());
@@ -246,7 +251,6 @@ public class CategoriesImplTest
then(authorityServiceMock).should().hasAdminAuthority();
then(authorityServiceMock).shouldHaveNoMoreInteractions();
then(nodesMock).should().isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false);
then(nodesMock).should().getNode(CATEGORY_ID);
then(nodesMock).shouldHaveNoMoreInteractions();
then(nodeServiceMock).should().getPrimaryParent(categoryNodeRef);
@@ -366,12 +370,151 @@ public class CategoriesImplTest
then(categoryServiceMock).shouldHaveNoInteractions();
}
@Test
public void testGetRootCategoryChildren()
{
final NodeRef parentCategoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PATH_ROOT);
given(categoryServiceMock.getRootCategoryNodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE))
.willReturn(Optional.of(parentCategoryNodeRef));
final int childrenCount = 3;
final List<ChildAssociationRef> childAssociationRefMocks = prepareChildAssocMocks(childrenCount, parentCategoryNodeRef);
given(nodeServiceMock.getChildAssocs(parentCategoryNodeRef)).willReturn(childAssociationRefMocks);
childAssociationRefMocks.forEach(this::prepareCategoryNodeMocks);
//when
final CollectionWithPagingInfo<Category> categoryChildren = objectUnderTest.getCategoryChildren(PATH_ROOT, parametersMock);
then(categoryServiceMock).should().getRootCategoryNodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);
then(categoryServiceMock).shouldHaveNoMoreInteractions();
then(nodeServiceMock).should().getChildAssocs(parentCategoryNodeRef);
childAssociationRefMocks.forEach(ca -> {
then(nodesMock).should().getNode(ca.getChildRef().getId());
then(nodeServiceMock).should()
.getChildAssocs(ca.getChildRef(), RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false);
then(nodeServiceMock).should().getPrimaryParent(ca.getChildRef());
});
then(nodeServiceMock).should(times(childrenCount)).getParentAssocs(parentCategoryNodeRef);
then(nodesMock).shouldHaveNoMoreInteractions();
then(nodeServiceMock).shouldHaveNoMoreInteractions();
then(authorityServiceMock).shouldHaveNoInteractions();
assertEquals(childAssociationRefMocks.size(), categoryChildren.getTotalItems().intValue());
final List<Category> categoryChildrenList = new ArrayList<>(categoryChildren.getCollection());
assertEquals(childAssociationRefMocks.size(), categoryChildrenList.size());
IntStream.range(0, childrenCount).forEach(i -> doCategoryAssertions(categoryChildrenList.get(i), i, PATH_ROOT));
}
@Test
public void testGetCategoryChildren()
{
final NodeRef parentCategoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_ID);
given(nodesMock.validateNode(PARENT_ID)).willReturn(parentCategoryNodeRef);
given(nodesMock.isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false)).willReturn(true);
final int childrenCount = 3;
final List<ChildAssociationRef> childAssociationRefMocks = prepareChildAssocMocks(childrenCount, parentCategoryNodeRef);
given(nodeServiceMock.getChildAssocs(parentCategoryNodeRef)).willReturn(childAssociationRefMocks);
childAssociationRefMocks.forEach(this::prepareCategoryNodeMocks);
//when
final CollectionWithPagingInfo<Category> categoryChildren = objectUnderTest.getCategoryChildren(PARENT_ID, parametersMock);
then(nodesMock).should().validateNode(PARENT_ID);
then(nodesMock).should().isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false);
then(nodeServiceMock).should().getChildAssocs(parentCategoryNodeRef);
childAssociationRefMocks.forEach(ca -> {
then(nodesMock).should().getNode(ca.getChildRef().getId());
then(nodeServiceMock).should()
.getChildAssocs(ca.getChildRef(), RegexQNamePattern.MATCH_ALL, RegexQNamePattern.MATCH_ALL, false);
then(nodeServiceMock).should().getPrimaryParent(ca.getChildRef());
});
then(nodeServiceMock).should(times(childrenCount)).getParentAssocs(parentCategoryNodeRef);
then(nodesMock).shouldHaveNoMoreInteractions();
then(nodeServiceMock).shouldHaveNoMoreInteractions();
then(authorityServiceMock).shouldHaveNoInteractions();
then(categoryServiceMock).shouldHaveNoInteractions();
assertEquals(childAssociationRefMocks.size(), categoryChildren.getTotalItems().intValue());
final List<Category> categoryChildrenList = new ArrayList<>(categoryChildren.getCollection());
assertEquals(childAssociationRefMocks.size(), categoryChildrenList.size());
IntStream.range(0, childrenCount).forEach(i -> doCategoryAssertions(categoryChildrenList.get(i), i, PARENT_ID));
}
@Test
public void testGetCategoryChildren_noChildren()
{
final NodeRef parentCategoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_ID);
given(nodesMock.validateNode(PARENT_ID)).willReturn(parentCategoryNodeRef);
given(nodesMock.isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false)).willReturn(true);
given(nodeServiceMock.getChildAssocs(parentCategoryNodeRef)).willReturn(Collections.emptyList());
//when
final CollectionWithPagingInfo<Category> categoryChildren = objectUnderTest.getCategoryChildren(PARENT_ID, parametersMock);
then(nodesMock).should().validateNode(PARENT_ID);
then(nodesMock).should().isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false);
then(nodesMock).shouldHaveNoMoreInteractions();
then(nodeServiceMock).should().getChildAssocs(parentCategoryNodeRef);
then(nodeServiceMock).shouldHaveNoMoreInteractions();
then(authorityServiceMock).shouldHaveNoInteractions();
then(categoryServiceMock).shouldHaveNoInteractions();
assertEquals(0, categoryChildren.getTotalItems().intValue());
}
@Test
public void testGetCategoryChildren_wrongParentNodeType()
{
final NodeRef parentCategoryNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_ID);
given(nodesMock.validateNode(PARENT_ID)).willReturn(parentCategoryNodeRef);
given(nodesMock.isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false)).willReturn(false);
//when
assertThrows(InvalidArgumentException.class, () -> objectUnderTest.getCategoryChildren(PARENT_ID, parametersMock));
then(nodesMock).should().validateNode(PARENT_ID);
then(nodesMock).should().isSubClass(parentCategoryNodeRef, ContentModel.TYPE_CATEGORY, false);
then(nodesMock).shouldHaveNoMoreInteractions();
then(nodeServiceMock).shouldHaveNoInteractions();
then(categoryServiceMock).shouldHaveNoInteractions();
then(authorityServiceMock).shouldHaveNoInteractions();
}
@Test
public void testGetCategoryChildren_nonExistingParentNode()
{
given(nodesMock.validateNode(PARENT_ID)).willThrow(EntityNotFoundException.class);
//when
assertThrows(EntityNotFoundException.class, () -> objectUnderTest.getCategoryChildren(PARENT_ID, parametersMock));
then(nodesMock).should().validateNode(PARENT_ID);
then(nodesMock).shouldHaveNoMoreInteractions();
then(nodeServiceMock).shouldHaveNoInteractions();
then(categoryServiceMock).shouldHaveNoInteractions();
then(authorityServiceMock).shouldHaveNoInteractions();
}
private Node prepareCategoryNode()
{
final Node categoryNode = new Node();
categoryNode.setName(CATEGORY_NAME);
categoryNode.setNodeId(CATEGORY_ID);
final NodeRef parentNodeRef = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, PARENT_ID);
return prepareCategoryNode(CATEGORY_NAME, CATEGORY_ID, parentNodeRef);
}
private Node prepareCategoryNode(final String name, final String id, final NodeRef parentNodeRef)
{
final Node categoryNode = new Node();
categoryNode.setName(name);
categoryNode.setNodeId(id);
categoryNode.setParentId(parentNodeRef);
return categoryNode;
}
@@ -387,4 +530,40 @@ public class CategoriesImplTest
.name(CATEGORY_NAME)
.create());
}
private List<ChildAssociationRef> prepareChildAssocMocks(final int count, NodeRef parentCategoryNodeRef)
{
return IntStream.range(0, count).mapToObj(i -> {
ChildAssociationRef dummyChildAssocMock = mock(ChildAssociationRef.class);
given(dummyChildAssocMock.getTypeQName()).willReturn(ContentModel.ASSOC_SUBCATEGORIES);
given(dummyChildAssocMock.getChildRef())
.willReturn(new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, CATEGORY_ID + "-" + i));
given(dummyChildAssocMock.getParentRef()).willReturn(parentCategoryNodeRef);
return dummyChildAssocMock;
})
.collect(Collectors.toList());
}
private void prepareCategoryNodeMocks(ChildAssociationRef childAssociationRef)
{
final NodeRef childRef = childAssociationRef.getChildRef();
final String id = childRef.getId();
final String name = id.replace(CATEGORY_ID, CATEGORY_NAME);
final NodeRef parentRef = childAssociationRef.getParentRef();
given(nodesMock.getNode(id)).willReturn(prepareCategoryNode(name, id, parentRef));
final ChildAssociationRef parentAssoc = new ChildAssociationRef(null, parentRef, null, childRef);
given(nodeServiceMock.getPrimaryParent(childRef)).willReturn(parentAssoc);
given(nodeServiceMock.getParentAssocs(parentRef)).willReturn(List.of(parentAssoc));
}
private void doCategoryAssertions(final Category category, final int index, final String parentId)
{
final Category expectedCategory = Category.builder()
.id(CATEGORY_ID + "-" + index)
.name(CATEGORY_NAME + "-" + index)
.parentId(parentId)
.hasChildren(false)
.create();
assertEquals(expectedCategory, category);
}
}