Replace gson mapper with jackson mapper
[cps.git] / cps-ri / src / test / groovy / org / onap / cps / spi / impl / CpsDataPersistenceServiceIntegrationSpec.groovy
index ad8db76..69e6aa8 100755 (executable)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021 Nordix Foundation
+ *  Copyright (C) 2021-2022 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021 Bell Canada.
  *  ================================================================================
  */
 package org.onap.cps.spi.impl
 
-import org.onap.cps.spi.exceptions.ConcurrencyException
-
-import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
-import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
-
+import com.fasterxml.jackson.databind.ObjectMapper
 import com.google.common.collect.ImmutableSet
-import com.google.gson.Gson
-import com.google.gson.GsonBuilder
 import org.onap.cps.spi.CpsDataPersistenceService
 import org.onap.cps.spi.entities.FragmentEntity
 import org.onap.cps.spi.exceptions.AlreadyDefinedException
@@ -37,17 +31,22 @@ import org.onap.cps.spi.exceptions.DataNodeNotFoundException
 import org.onap.cps.spi.exceptions.DataspaceNotFoundException
 import org.onap.cps.spi.model.DataNode
 import org.onap.cps.spi.model.DataNodeBuilder
+import org.onap.cps.utils.JsonObjectMapper
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.test.context.jdbc.Sql
 
 import javax.validation.ConstraintViolationException
+import java.util.stream.Collectors
+
+import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
 
 class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
 
     @Autowired
     CpsDataPersistenceService objectUnderTest
 
-    static final Gson GSON = new GsonBuilder().create()
+    static final JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
 
     static final String SET_DATA = '/data/fragment.sql'
     static final long ID_DATA_NODE_WITH_DESCENDANTS = 4001
@@ -55,7 +54,12 @@ 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_PARENT204_FRAGMENT_ID = 4219L
+    static final long LIST_DATA_NODE_PARENT205_FRAGMENT_ID = 4221L
+    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
@@ -152,29 +156,37 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Add list-node fragment with multiple elements.'() {
-        given: 'list node data fragment as a collection of data nodes'
-            def listNodeXpaths = ['/parent-201/child-204[@key="B"]', '/parent-201/child-204[@key="C"]']
-            def listNodeCollection = buildDataNodeCollection(listNodeXpaths)
-        when: 'list-node elements added to existing parent node'
-            objectUnderTest.addListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, '/parent-201', listNodeCollection)
+    def 'Add multiple list elements including an element with a child datanode.'() {
+        given: 'two new data nodes for an existing list'
+            def listElementXpaths = ['/parent-201/child-204[@key="B"]', '/parent-201/child-204[@key="C"]']
+            def listElements = toDataNodes(listElementXpaths)
+        and: 'a child node for one of the new data nodes'
+            def childDataNode = buildDataNode('/parent-201/child-204[@key="C"]/grand-child-204[@key2="Z"]', [leave:'value'], [])
+            listElements[0].childDataNodes = [childDataNode]
+        when: 'the data nodes (list elements) are added to existing parent node'
+            objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME3, '/parent-201', listElements)
         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)
+            assert allChildXpaths.containsAll(listElementXpaths)
+        and: 'the child node of the new list entry is also present'
+            def dataspaceEntity = dataspaceRepository.getByName(DATASPACE_NAME)
+            def anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, ANCHOR_NAME3)
+            def listElementChild = fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, childDataNode.xpath)
+            assert listElementChild.isPresent()
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Add list-node fragment error scenario: #scenario.'() {
-        given: 'list node data fragment as a collection of data nodes'
-            def listNodeCollection = buildDataNodeCollection(listNodeXpaths)
-        when: 'list-node elements added to existing parent node'
-            objectUnderTest.addListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, parentNodeXpath, listNodeCollection)
+    def 'Add list element error scenario: #scenario.'() {
+        given: 'list element as a collection of data nodes'
+            def listElementCollection = toDataNodes(listElementXpaths)
+        when: 'attempt to add list elements to parent node'
+            objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME3, parentNodeXpath, listElementCollection)
         then: 'a #expectedException is thrown'
             thrown(expectedException)
         where: 'following parameters were used'
-            scenario                     | parentNodeXpath | listNodeXpaths                      || expectedException
+            scenario                     | parentNodeXpath | listElementXpaths                   || expectedException
             'parent node does not exist' | '/unknown'      | ['irrelevant']                      || DataNodeNotFoundException
             'already existing fragment'  | '/parent-201'   | ['/parent-201/child-204[@key="A"]'] || AlreadyDefinedException
 
@@ -253,7 +265,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 +296,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 +310,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 +335,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 +355,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 +374,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
@@ -372,36 +384,163 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Replace list-node content of #scenario.'() {
-        given: 'list node data fragment as a collection of data nodes'
-            def listNodeCollection = buildDataNodeCollection(listNodeXpaths)
-        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 'Replace list content of #scenario.'() {
+        given: 'list element as a collection of data nodes'
+            def listElementCollection = toDataNodes(listElementXpaths)
+        when: 'list elements are replaced within the existing parent node'
+            objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, parentXpath, listElementCollection)
+        then: 'list elements are updated as expected, non-list element remains as is'
+            def parentFragment = fragmentRepository.getById(listElementFragmentID)
             def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
             assert allChildXpaths.size() == expectedChildXpaths.size()
             assert allChildXpaths.containsAll(expectedChildXpaths)
         where: 'following parameters were used'
-            scenario                 | listNodeXpaths                      || expectedChildXpaths
-            'existing list-node'     | ['/parent-201/child-204[@key="B"]'] || ['/parent-201/child-203', '/parent-201/child-204[@key="B"]']
-            'non-existing list-node' | ['/parent-201/child-205[@key="1"]'] || ['/parent-201/child-203', '/parent-201/child-204[@key="A"]', '/parent-201/child-204[@key="X"]', '/parent-201/child-205[@key="1"]']
+            scenario                                                    | listElementXpaths                                                          | parentXpath             | listElementFragmentID                 || expectedChildXpaths
+            'existing list element with non existing key'               | ['/parent-201/child-204[@key="B"]']                                        | '/parent-201'           | LIST_DATA_NODE_PARENT201_FRAGMENT_ID  || ['/parent-201/child-203', '/parent-201/child-204[@key="B"]']
+            'non existing list element with non existing key'           | ['/parent-201/child-205[@key="1"]']                                        | '/parent-201'           | LIST_DATA_NODE_PARENT201_FRAGMENT_ID  || ['/parent-201/child-203', '/parent-201/child-204[@key="A"]', '/parent-201/child-204[@key="X"]', '/parent-201/child-205[@key="1"]']
+            'list element with 1 existing key'                          | ['/parent-201/child-204[@key="X"]']                                        | '/parent-201'           | LIST_DATA_NODE_PARENT201_FRAGMENT_ID  || ['/parent-201/child-203', '/parent-201/child-204[@key="X"]']
+            'list element with combined keys'                           | ['/parent-202/child-205[@key="A"]']                                        | '/parent-202'           | LIST_DATA_NODE_PARENT202_FRAGMENT_ID  || ['/parent-202/child-206[@key="A"]', '/parent-202/child-205[@key="A"]']
+            'grandchild list element'                                   | ['/parent-200/child-202/grand-child-202[@key="E"]']                        | '/parent-200/child-202' | LIST_DATA_NODE_CHILD202_FRAGMENT_ID   || ['/parent-200/child-202/grand-child-202[@key="E"]']
+            'list element with two list elements'                       | ['/parent-201/child-204[@key="new X"]', '/parent-201/child-204[@key="Y"]'] | '/parent-201'           | LIST_DATA_NODE_PARENT201_FRAGMENT_ID  || ['/parent-201/child-203', '/parent-201/child-204[@key="new X"]', '/parent-201/child-204[@key="Y"]']
+            'list element with compounded list element'                 | ['/parent-202/child-205[@key="A" and @key2="B"]']                          | '/parent-202'           | LIST_DATA_NODE_PARENT202_FRAGMENT_ID  || ['/parent-202/child-206[@key="A"]', '/parent-202/child-205[@key="A" and @key2="B"]']
+            'list element with list element with parent with key value' | ['/parent-204[@key="L"]/child-210[@key="N"]']                              | '/parent-204[@key="L"]' | LIST_DATA_NODE_PARENT204_FRAGMENT_ID  || ['/parent-204[@key="L"]/child-210[@key="N"]']
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Replace list-node fragment error scenario: #scenario.'() {
-        given: 'list node data fragment as a collection of data nodes'
-            def listNodeCollection = buildDataNodeCollection(listNodeXpaths)
-        when: 'list-node elements were replaced under existing parent node'
-            objectUnderTest.replaceListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, parentNodeXpath, listNodeCollection)
+    def 'Replace list content that has #scenario'() {
+        given: 'list element with child list element as a collection of data nodes'
+            def grandChildDataNodes = toDataNodes(grandChildXpaths)
+            def listElementCollection = new DataNodeBuilder().withXpath(childXpath).withChildDataNodes(grandChildDataNodes).build()
+        when: 'list elements replaced within the existing parent node'
+            objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, parentXpath, [listElementCollection ])
+        then: 'list elements are updated as expected with non-list elements remaining as is'
+            def parentFragment = fragmentRepository.getById(listElementFragmentId)
+            def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
+            assert allChildXpaths.size() == expectedRemainingChildXpaths.size()
+            assert allChildXpaths.containsAll(expectedRemainingChildXpaths)
+        and: 'grandchild list elements are updated as expected'
+            def allGrandChildXpaths = parentFragment.getChildFragments().collect(){
+                it.getChildFragments().collect(){
+                    it.getXpath()}}
+            allGrandChildXpaths.removeIf(list -> list.isEmpty())
+            def grandChildXpathsToList = allGrandChildXpaths.stream().flatMap(List::stream).collect(Collectors.toList())
+            def expectedGrandChildXpaths = grandChildXpaths
+            assert grandChildXpathsToList.size() == expectedGrandChildXpaths.size()
+            assert grandChildXpathsToList.containsAll(expectedGrandChildXpaths)
+        where: 'the following parameters are used'
+            scenario                                  | parentXpath   | childXpath                        | grandChildXpaths                                                                              | listElementFragmentId                || expectedRemainingChildXpaths
+            'grandchild of list'                      | '/parent-203' | '/parent-203/child-204[@key="X"]' | ['/parent-203/child-204/grandchild[@key="2"]']                                                | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="X"]']
+            'grandchild of list with two new element' | '/parent-203' | '/parent-203/child-204[@key="X"]' | ['/parent-203/child-204/grandchild[@key="2"]' , '/parent-203/child-204/grandchild[@key="3"]'] | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="X"]']
+            'grandchild with compound list elements'  | '/parent-205' | '/parent-205/child-205[@key="X"]' | ['/parent-205/child-205/grand-child-206[@key="Y" and @key2="Z"]']                             | LIST_DATA_NODE_PARENT205_FRAGMENT_ID || ['/parent-205/child-205', '/parent-205/child-205[@key="X"]']
+    }
+
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Replace list content of #scenario with grandchildren.'() {
+        given: 'list element as a collection of data nodes'
+            def listElementCollection = toDataNodes(listElementXpaths)
+        when: 'list elements are replaced within the existing parent node'
+            objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, parentXpath, listElementCollection)
+        then: 'child list elements are updated as expected with non-list elements remaining as is'
+            def parentFragment = fragmentRepository.getById(listElementFragmentID)
+            def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
+            assert allChildXpaths.size() == expectedChildXpaths.size()
+            assert allChildXpaths.containsAll(expectedChildXpaths)
+        and: 'grandchild list elements are updated as expected'
+            def allGrandChildXpaths = parentFragment.getChildFragments().collect {
+                it.getChildFragments().collect {
+                    it.getXpath()}}
+            allGrandChildXpaths.removeIf(list -> list.isEmpty())
+            assert allGrandChildXpaths.size() == expectedGrandChildXpaths.size()
+            assert allGrandChildXpaths.containsAll(expectedGrandChildXpaths)
+        where: 'following parameters were used'
+            scenario                                       | listElementXpaths                   | parentXpath   | listElementFragmentID                || expectedChildXpaths                                          | expectedGrandChildXpaths
+            'existing list element with existing keys'     | ['/parent-203/child-204[@key="X"]'] | '/parent-203' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="X"]'] | []
+            'non existing list element with existing keys' | ['/parent-203/child-204[@key="V"]'] | '/parent-203' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="V"]'] | []
+    }
+
+
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Replace content error scenario: #scenario.'() {
+        given: 'list element as a collection of data nodes'
+            def listElementCollection = toDataNodes(listElementXpaths)
+        when: 'list elements were replaced under existing parent node'
+            objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, parentNodeXpath, listElementCollection)
         then: 'a #expectedException is thrown'
             thrown(expectedException)
         where: 'following parameters were used'
-            scenario                     | parentNodeXpath | listNodeXpaths || expectedException
+            scenario                     | parentNodeXpath | listElementXpaths || expectedException
             'parent node does not exist' | '/unknown'      | ['irrelevant'] || DataNodeNotFoundException
     }
 
-    static Collection<DataNode> buildDataNodeCollection(xpaths) {
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Delete list scenario: #scenario.'() {
+        when: 'deleting list is executed for: #scenario.'
+            objectUnderTest.deleteListDataNode(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths)
+        then: 'only the expected children remain'
+            def parentFragment = fragmentRepository.getById(parentFragmentId)
+            def remainingChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
+            assert remainingChildXpaths.size() == expectedRemainingChildXpaths.size()
+            assert remainingChildXpaths.containsAll(expectedRemainingChildXpaths)
+        where: 'following parameters were used'
+            scenario                          | targetXpaths                                                 | parentFragmentId                     || expectedRemainingChildXpaths
+            'list element with key'           | '/parent-203/child-204[@key="A"]'                            | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="X"]']
+            'list element with combined keys' | '/parent-202/child-205[@key="A" and @key2="B"]'              | LIST_DATA_NODE_PARENT202_FRAGMENT_ID || ['/parent-202/child-206[@key="A"]']
+            'whole list'                      | '/parent-203/child-204'                                      | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203']
+            'list element under list element' | '/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"]']
+    }
+
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Delete list error scenario: #scenario.'() {
+        when: 'attempting to delete scenario: #scenario.'
+            objectUnderTest.deleteListDataNode(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths)
+        then: 'a DataNodeNotFoundException is thrown'
+            thrown(DataNodeNotFoundException)
+        where: 'following parameters were used'
+            scenario                                   | targetXpaths
+            'whole list, parent node does not exist'   | '/unknown/some-child'
+            'list element, parent node does not exist' | '/unknown/child-204[@key="A"]'
+            'whole list does not exist'                | '/parent-200/unknown'
+            'list element, list does not exist'        | '/parent-200/unknown[@key="C"]'
+            'list element, element does not exist'     | '/parent-203/child-204[@key="C"]'
+            'valid datanode but not a list'            | '/parent-200/child-202'
+    }
+
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Confirm deletion of #scenario.'() {
+        given: 'a valid data node'
+            def dataNode
+            def dataNodeXpath
+        when: 'data nodes are deleted'
+            objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion)
+        then: 'verify data nodes are removed'
+            try {
+                dataNode = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, getDataNodesXpaths, INCLUDE_ALL_DESCENDANTS)
+                dataNodeXpath = dataNode.getXpath()
+                assert dataNodeXpath == expectedXpaths
+            } catch (DataNodeNotFoundException) {
+                assert dataNodeXpath == expectedXpaths
+            }
+        where: 'following parameters were used'
+            scenario                                | xpathForDeletion                                   | getDataNodesXpaths                                || expectedXpaths
+            'child of target'                       | '/parent-206/child-206'                            | '/parent-206/child-206'                           || null
+            'child data node, parent still exists'  | '/parent-206/child-206'                            | '/parent-206'                                     || '/parent-206'
+            'list element'                          | '/parent-206/child-206/grand-child-206[@key="A"]'  | '/parent-206/child-206/grand-child-206[@key="A"]' || null
+            'list element, sibling still exists'    | '/parent-206/child-206/grand-child-206[@key="A"]'  | '/parent-206/child-206/grand-child-206[@key="X"]' || '/parent-206/child-206/grand-child-206[@key="X"]'
+    }
+
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Delete data node with #scenario.'() {
+        when: 'data node is deleted'
+            objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, datanodeXpath)
+        then: 'a #expectedException is thrown'
+            thrown(DataNodeNotFoundException)
+        where: 'the following parameters were used'
+            scenario                                        | datanodeXpath
+            'valid data node, non existent child node'      | '/parent-203/child-non-existent'
+            'invalid list element'                          | '/parent-206/child-206/grand-child-206@key="A"]'
+    }
+
+    static Collection<DataNode> toDataNodes(xpaths) {
         return xpaths.collect { new DataNodeBuilder().withXpath(it).build() }
     }
 
@@ -410,7 +549,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     }
 
     static Map<String, Object> getLeavesMap(FragmentEntity fragmentEntity) {
-        return GSON.fromJson(fragmentEntity.getAttributes(), Map<String, Object>.class)
+        return jsonObjectMapper.convertJsonString(fragmentEntity.getAttributes(), Map<String, Object>.class)
     }
 
     def static assertLeavesMaps(actualLeavesMap, expectedLeavesMap) {