Delete list-node p1 service and persistence layers 81/122881/6
authorlukegleeson <luke.gleeson@est.tech>
Tue, 27 Jul 2021 14:07:05 +0000 (15:07 +0100)
committerlukegleeson <luke.gleeson@est.tech>
Tue, 17 Aug 2021 09:26:50 +0000 (10:26 +0100)
Persistence Layer of delete List-Node Content
Service Layer of delete List-Node Content
Updating of deprecated FragementRepository.getOne() method to FragmentRepository.getByID() in CpsDataPersistenceServiceIntegration.groovy
Remove Runtime Exceptions thrown from CpsDataService.java javadoc

Issue-ID: CPS-361
Signed-off-by: lukegleeson <luke.gleeson@est.tech>
Change-Id: Ib5762e73a6e8620c50c2e07b00086b9287770bc3

cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy
cps-ri/src/test/resources/data/fragment.sql
cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy

index c638b91..af010f4 100644 (file)
@@ -34,6 +34,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import javax.transaction.Transactional;
@@ -48,6 +49,7 @@ import org.onap.cps.spi.entities.FragmentEntity;
 import org.onap.cps.spi.exceptions.AlreadyDefinedException;
 import org.onap.cps.spi.exceptions.ConcurrencyException;
 import org.onap.cps.spi.exceptions.CpsPathException;
+import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
 import org.onap.cps.spi.model.DataNode;
 import org.onap.cps.spi.model.DataNodeBuilder;
 import org.onap.cps.spi.repository.AnchorRepository;
@@ -82,6 +84,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
 
     private static final Gson GSON = new GsonBuilder().create();
     private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@[\\s\\S]+?]){0,1})";
+    private static final String REG_EX_FOR_LIST_NODE_KEY = "\\[(\\@([^/]*?))+( and)*\\]$";
 
     @Override
     public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentXpath,
@@ -315,8 +318,33 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         fragmentRepository.save(parentEntity);
     }
 
+    @Override
+    @Transactional
+    public void deleteListDataNodes(final String dataspaceName, final String anchorName, final String listNodeXpath) {
+        final var parentNodeXpath = listNodeXpath.substring(0, listNodeXpath.lastIndexOf('/'));
+        final var parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
+        final var descendantNode = listNodeXpath.substring(listNodeXpath.lastIndexOf('/'));
+        final Matcher descendantNodeHasListNodeKey = Pattern.compile(REG_EX_FOR_LIST_NODE_KEY).matcher(descendantNode);
+
+        final boolean xpathPointsToAValidChildNodeWithKey = parentEntity.getChildFragments().stream().anyMatch(
+            (fragment) -> fragment.getXpath().equals(listNodeXpath));
+
+        final boolean xpathPointsToAValidChildNodeWithoutKey = parentEntity.getChildFragments().stream().anyMatch(
+            (fragment) -> fragment.getXpath().replaceAll(REG_EX_FOR_LIST_NODE_KEY, "").equals(listNodeXpath));
+
+        if ((descendantNodeHasListNodeKey.find() && xpathPointsToAValidChildNodeWithKey)
+            ||
+            (!descendantNodeHasListNodeKey.find() && xpathPointsToAValidChildNodeWithoutKey)) {
+            removeListNodeDescendants(parentEntity, listNodeXpath);
+        } else {
+            throw new DataNodeNotFoundException(parentEntity.getDataspace().getName(),
+                parentEntity.getAnchor().getName(), listNodeXpath);
+        }
+    }
+
     private void removeListNodeDescendants(final FragmentEntity parentFragmentEntity, final String listNodeXpath) {
-        final String listNodeXpathPrefix = listNodeXpath + "[";
+        final Matcher descendantNodeHasListNodeKey = Pattern.compile(REG_EX_FOR_LIST_NODE_KEY).matcher(listNodeXpath);
+        final String listNodeXpathPrefix = listNodeXpath + (descendantNodeHasListNodeKey.find() ? "" : "[");
         if (parentFragmentEntity.getChildFragments()
             .removeIf(fragment -> fragment.getXpath().startsWith(listNodeXpathPrefix))) {
             fragmentRepository.save(parentFragmentEntity);
index ad8db76..8217a4f 100755 (executable)
@@ -21,7 +21,7 @@
  */
 package org.onap.cps.spi.impl
 
-import org.onap.cps.spi.exceptions.ConcurrencyException
+import org.onap.cps.spi.exceptions.DataValidationException
 
 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
@@ -55,7 +55,10 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     static final String XPATH_DATA_NODE_WITH_LEAVES = '/parent-100'
     static final long UPDATE_DATA_NODE_FRAGMENT_ID = 4202L
     static final long UPDATE_DATA_NODE_SUB_FRAGMENT_ID = 4203L
-    static final long LIST_DATA_NODE_PARENT_FRAGMENT_ID = 4206L
+    static final long LIST_DATA_NODE_PARENT201_FRAGMENT_ID = 4206L
+    static final long LIST_DATA_NODE_PARENT203_FRAGMENT_ID = 4214L
+    static final long LIST_DATA_NODE_CHILD202_FRAGMENT_ID = 4204L
+    static final long LIST_DATA_NODE_PARENT202_FRAGMENT_ID = 4211L
 
     static final DataNode newDataNode = new DataNodeBuilder().build()
     static DataNode existingDataNode
@@ -159,7 +162,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
         when: 'list-node elements added to existing parent node'
             objectUnderTest.addListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, '/parent-201', listNodeCollection)
         then: 'new entries successfully persisted, parent node now contains 5 children (2 new + 3 existing before)'
-            def parentFragment = fragmentRepository.getOne(LIST_DATA_NODE_PARENT_FRAGMENT_ID)
+            def parentFragment = fragmentRepository.getById(LIST_DATA_NODE_PARENT201_FRAGMENT_ID)
             def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
             assert allChildXpaths.size() == 5
             assert allChildXpaths.containsAll(listNodeXpaths)
@@ -253,7 +256,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
             objectUnderTest.updateDataLeaves(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
                     "/parent-200/child-201", ['leaf-value': 'new'])
         then: 'leaves are updated for selected data node'
-            def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
+            def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
             def updatedLeaves = getLeavesMap(updatedFragment)
             assert updatedLeaves.size() == 1
             assert updatedLeaves.'leaf-value' == 'new'
@@ -284,7 +287,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
         when: 'replace data node tree is performed'
             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
         then: 'leaves have been updated for selected data node'
-            def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
+            def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
             def updatedLeaves = getLeavesMap(updatedFragment)
             assert updatedLeaves.size() == 1
             assert updatedLeaves.'leaf-value' == 'new'
@@ -298,12 +301,12 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     def 'Replace data node tree with descendants.'() {
         given: 'data node object with leaves updated, having child with old content'
             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
-                    buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'original'], [])
+                  buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'original'], [])
             ])
         when: 'update is performed including descendants'
             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
         then: 'leaves have been updated for selected data node'
-            def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
+            def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
             def updatedLeaves = getLeavesMap(updatedFragment)
             assert updatedLeaves.size() == 1
             assert updatedLeaves.'leaf-value' == 'new'
@@ -323,7 +326,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
         when: 'update is performed including descendants'
             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
         then: 'leaves have been updated for selected data node'
-            def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
+            def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
             def updatedLeaves = getLeavesMap(updatedFragment)
             assert updatedLeaves.size() == 1
             assert updatedLeaves.'leaf-value' == 'new'
@@ -343,7 +346,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
         when: 'update is performed including descendants'
             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
         then: 'leaves have been updated for selected data node'
-            def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
+            def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
             def updatedLeaves = getLeavesMap(updatedFragment)
             assert updatedLeaves.size() == 1
             assert updatedLeaves.'leaf-value' == 'new'
@@ -362,7 +365,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
             def submittedDataNode = buildDataNode(xpath, ['leaf-name': 'leaf-value'], [])
         when: 'attempt to update data node for #scenario'
             objectUnderTest.replaceDataNodeTree(dataspaceName, anchorName, submittedDataNode)
-        then: 'a #expectedException is thrown'  
+        then: 'a #expectedException is thrown'
             thrown(expectedException)
         where: 'the following data is used'
             scenario                 | dataspaceName  | anchorName                        | xpath                || expectedException
@@ -378,7 +381,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
         when: 'list-node elements replaced within the existing parent node'
             objectUnderTest.replaceListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, '/parent-201', listNodeCollection)
         then: 'child list elements are updated as expected, non-list element remains as is'
-            def parentFragment = fragmentRepository.getOne(LIST_DATA_NODE_PARENT_FRAGMENT_ID)
+            def parentFragment = fragmentRepository.getById(LIST_DATA_NODE_PARENT201_FRAGMENT_ID)
             def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
             assert allChildXpaths.size() == expectedChildXpaths.size()
             assert allChildXpaths.containsAll(expectedChildXpaths)
@@ -401,6 +404,47 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
             'parent node does not exist' | '/unknown'      | ['irrelevant'] || DataNodeNotFoundException
     }
 
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Delete list-node content of #scenario.'() {
+        given: 'list node data fragments are present in database'
+        when: 'list-node elements deleted within the existing parent node'
+            objectUnderTest.deleteListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, listNodeXpaths)
+        then: 'child list elements are removed as expected, non-list element remains as is'
+            def parentFragment = fragmentRepository.getById(listNodeFragmentID)
+            def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
+            assert allChildXpaths.size() == expectedChildXpaths.size()
+            assert allChildXpaths.containsAll(expectedChildXpaths)
+        where: 'following parameters were used'
+            scenario                                          | listNodeXpaths                                               | listNodeFragmentID                   || expectedChildXpaths
+            'existing list-node with key'                     | '/parent-203/child-204[@key="A"]'                            | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="X"]']
+            'existing list-node with key'                     | '/parent-203/child-204[@key="X"]'                            | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="A"]']
+            'existing grand-child list node with keys'        | '/parent-203/child-204[@key="X"]/grand-child-204[@key2="Y"]' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="X"]', '/parent-203/child-204[@key="A"]']
+            'existing list-node with combined keys'           | '/parent-202/child-205[@key="A" and @key2="B"]'              | LIST_DATA_NODE_PARENT202_FRAGMENT_ID || ['/parent-202/child-206[@key="A"]']
+            'existing node with list node variants to delete' | '/parent-203/child-204'                                      | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203']
+            'existing grandchild list-node'                   | '/parent-200/child-202/grand-child-202[@key="D"]'            | LIST_DATA_NODE_CHILD202_FRAGMENT_ID  || []
+    }
+
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Delete list-node fragment error scenario: #scenario.'() {
+        given: 'list node data fragments are present in database'
+        when: 'list-node elements are deleted under existing parent node'
+            objectUnderTest.deleteListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, listNodeXpaths)
+        then: 'a #expectedException is thrown'
+            thrown(expectedException)
+        where: 'following parameters were used'
+            scenario                                            | listNodeXpaths                                    || expectedException
+            'list parent node does not exist'                   | '/unknown/unknown'                                || DataNodeNotFoundException
+            'list child nodes do not exist'                     | '/parent-200/unknown'                             || DataNodeNotFoundException
+            'list child nodes with key does not exist'          | '/parent-200/unknown[@key="C"]'                   || DataNodeNotFoundException
+            'list grandchild nodes parent does not exist'       | '/parent-200/unknown/unknown'                     || DataNodeNotFoundException
+            'non-existing parent with existing list-node'       | '/unknown/child-204'                              || DataNodeNotFoundException
+            'non-existing parent with existing list-node & key' | '/unknown/child-204[@key="A"]'                    || DataNodeNotFoundException
+            'valid with non existing key'                       | '/parent-200/child-202/grand-child-202[@key="A"]' || DataNodeNotFoundException
+            'child list node without key'                       | '/parent-200/child-204/grand-child-204'           || DataNodeNotFoundException
+            'valid list node with invalid key'                  | '/parent-203/child-204[@key="C"]'                 || DataNodeNotFoundException
+
+    }
+
     static Collection<DataNode> buildDataNodeCollection(xpaths) {
         return xpaths.collect { new DataNodeBuilder().withXpath(it).build() }
     }
index d7109f2..886e6e1 100755 (executable)
@@ -49,8 +49,16 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES)
     (4202, 1001, 3003, 4201, '/parent-200/child-201', '{"leaf-value": "original"}'),
     (4203, 1001, 3003, 4202, '/parent-200/child-201/grand-child', '{"leaf-value": "original"}'),
     (4204, 1001, 3003, 4201, '/parent-200/child-202', '{"common-leaf-name": "common-leaf value", "common-leaf-name-int" : 5}'),
-    (4205, 1001, 3003, 4204, '/parent-200/child-202/grand-child-202', '{"common-leaf-name": "common-leaf value", "common-leaf-name-int" : 5}'),
+    (4205, 1001, 3003, 4204, '/parent-200/child-202/grand-child-202[@key="D"]', '{"common-leaf-name": "common-leaf value", "common-leaf-name-int" : 5}'),
     (4206, 1001, 3003, null, '/parent-201', '{"leaf-value": "original"}'),
     (4207, 1001, 3003, 4206, '/parent-201/child-203', '{}'),
     (4208, 1001, 3003, 4206, '/parent-201/child-204[@key="A"]', '{"key": "A"}'),
-    (4209, 1001, 3003, 4206, '/parent-201/child-204[@key="X"]', '{"key": "X"}');
+    (4209, 1001, 3003, 4206, '/parent-201/child-204[@key="X"]', '{"key": "X"}'),
+    (4211, 1001, 3003, null, '/parent-202', '{"leaf-value": "original"}'),
+    (4212, 1001, 3003, 4211, '/parent-202/child-205[@key="A" and @key2="B"]', '{"key": "A", "key2": "B"}'),
+    (4213, 1001, 3003, 4211, '/parent-202/child-206[@key="A"]', '{"key": "A"}'),
+    (4214, 1001, 3003, null, '/parent-203', '{"leaf-value": "original"}'),
+    (4215, 1001, 3003, 4214, '/parent-203/child-203', '{}'),
+    (4216, 1001, 3003, 4214, '/parent-203/child-204[@key="A"]', '{"key": "A"}'),
+    (4217, 1001, 3003, 4214, '/parent-203/child-204[@key="X"]', '{"key": "X"}'),
+    (4218, 1001, 3003, 4217, '/parent-203/child-204[@key="X"]/grand-child-204[@key2="Y"]', '{"key": "X", "key2": "Y"}');
\ No newline at end of file
index 7187810..6036f92 100644 (file)
@@ -23,9 +23,6 @@ package org.onap.cps.api;
 
 import org.checkerframework.checker.nullness.qual.NonNull;
 import org.onap.cps.spi.FetchDescendantsOption;
-import org.onap.cps.spi.exceptions.AlreadyDefinedException;
-import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
-import org.onap.cps.spi.exceptions.DataValidationException;
 import org.onap.cps.spi.model.DataNode;
 
 /*
@@ -39,7 +36,6 @@ public interface CpsDataService {
      * @param dataspaceName dataspace name
      * @param anchorName    anchor name
      * @param jsonData      json data
-     * @throws DataValidationException when json data is invalid
      */
     void saveData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String jsonData);
 
@@ -50,9 +46,6 @@ public interface CpsDataService {
      * @param anchorName      anchor name
      * @param parentNodeXpath parent node xpath
      * @param jsonData        json data
-     * @throws DataValidationException   when json data is invalid
-     * @throws DataNodeNotFoundException when parent node cannot be found by parent node xpath
-     * @throws AlreadyDefinedException   when child data node with same xpath already exists
      */
     void saveData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath,
         @NonNull String jsonData);
@@ -65,9 +58,6 @@ public interface CpsDataService {
      * @param anchorName      anchor name
      * @param parentNodeXpath parent node xpath
      * @param jsonData        json data representing list element
-     * @throws DataValidationException   when json data is invalid (incl. list-node being empty)
-     * @throws DataNodeNotFoundException when parent node cannot be found by parent node xpath
-     * @throws AlreadyDefinedException   when any of child data nodes is having xpath of already existing node
      */
     void saveListNodeData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath,
         @NonNull String jsonData);
@@ -115,9 +105,17 @@ public interface CpsDataService {
      * @param anchorName      anchor name
      * @param parentNodeXpath parent node xpath
      * @param jsonData        json data representing list element
-     * @throws DataValidationException   when json data is invalid (incl. list-node being empty)
-     * @throws DataNodeNotFoundException when parent node cannot be found by parent node xpath
      */
     void replaceListNodeData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath,
         @NonNull String jsonData);
+
+    /**
+     * Deletes (if exists) child data fragment representing list-node (with one or more elements)
+     * under existing data node for the given anchor and dataspace.
+     *
+     * @param dataspaceName   dataspace name
+     * @param anchorName      anchor name
+     * @param listNodeXpath   list node xpath
+     */
+    void deleteListNodeData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String listNodeXpath);
 }
index 717df16..5e6e1a2 100755 (executable)
@@ -114,6 +114,13 @@ public class CpsDataServiceImpl implements CpsDataService {
         notificationService.processDataUpdatedEvent(dataspaceName, anchorName);
     }
 
+    @Override
+    public void deleteListNodeData(final String dataspaceName, final String anchorName, final String listNodeXpath) {
+        cpsDataPersistenceService.deleteListDataNodes(dataspaceName, anchorName, listNodeXpath);
+        notificationService.processDataUpdatedEvent(dataspaceName, anchorName);
+    }
+
+
     private DataNode buildDataNodeFromJson(final String dataspaceName, final String anchorName,
         final String parentNodeXpath, final String jsonData) {
 
index cfb39f5..bf8dd1a 100644 (file)
@@ -111,6 +111,16 @@ public interface CpsDataPersistenceService {
     void replaceListDataNodes(@NonNull String dataspaceName, @NonNull String anchorName,
         @NonNull String parentNodeXpath, @NonNull Collection<DataNode> dataNodes);
 
+    /**
+     * Deletes existing list data node content including descendants.
+     *
+     * @param dataspaceName   dataspace name
+     * @param anchorName      anchor name
+     * @param listNodeXpath   list node xpath
+     */
+    void deleteListDataNodes(@NonNull String dataspaceName, @NonNull String anchorName,
+                              @NonNull String listNodeXpath);
+
     /**
      * Get a datanode by cps path.
      *
index 19ccee3..1220397 100644 (file)
@@ -28,6 +28,7 @@ import org.onap.cps.api.CpsModuleService
 import org.onap.cps.notification.NotificationService
 import org.onap.cps.spi.CpsDataPersistenceService
 import org.onap.cps.spi.FetchDescendantsOption
+import org.onap.cps.spi.exceptions.CpsPathException
 import org.onap.cps.spi.exceptions.DataValidationException
 import org.onap.cps.spi.model.Anchor
 import org.onap.cps.spi.model.DataNodeBuilder
@@ -197,6 +198,17 @@ class CpsDataServiceImplSpec extends Specification {
             thrown(DataValidationException)
     }
 
+    def 'Delete list-node data fragment under existing node.'() {
+        given: 'schema set for given anchor and dataspace references test-tree model'
+            setupSchemaSetMocks('test-tree.yang')
+        when: 'delete list data method is invoked with list-node json data'
+            objectUnderTest.deleteListNodeData(dataspaceName, anchorName, '/test-tree/branch')
+        then: 'the persistence service method is invoked with correct parameters'
+            1 * mockCpsDataPersistenceService.deleteListDataNodes(dataspaceName, anchorName, '/test-tree/branch')
+        and: 'data updated event is sent to notification service'
+            1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName)
+    }
+
     def setupSchemaSetMocks(String... yangResources) {
         def anchor = Anchor.builder().name(anchorName).schemaSetName(schemaSetName).build()
         mockCpsAdminService.getAnchor(dataspaceName, anchorName) >> anchor