Merge "Add test for missing code covereage"
[cps.git] / cps-ri / src / test / groovy / org / onap / cps / spi / impl / CpsDataPersistenceServiceIntegrationSpec.groovy
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2021 Nordix Foundation
4  *  Modifications Copyright (C) 2021 Pantheon.tech
5  *  Modifications Copyright (C) 2021 Bell Canada.
6  *  ================================================================================
7  *  Licensed under the Apache License, Version 2.0 (the "License");
8  *  you may not use this file except in compliance with the License.
9  *  You may obtain a copy of the License at
10  *
11  *        http://www.apache.org/licenses/LICENSE-2.0
12  *
13  *  Unless required by applicable law or agreed to in writing, software
14  *  distributed under the License is distributed on an "AS IS" BASIS,
15  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  *  See the License for the specific language governing permissions and
17  *  limitations under the License.
18  *
19  *  SPDX-License-Identifier: Apache-2.0
20  *  ============LICENSE_END=========================================================
21  */
22 package org.onap.cps.spi.impl
23
24 import com.google.common.collect.ImmutableSet
25 import com.google.gson.Gson
26 import com.google.gson.GsonBuilder
27 import org.onap.cps.spi.CpsDataPersistenceService
28 import org.onap.cps.spi.entities.FragmentEntity
29 import org.onap.cps.spi.exceptions.AlreadyDefinedException
30 import org.onap.cps.spi.exceptions.AnchorNotFoundException
31 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
32 import org.onap.cps.spi.exceptions.DataspaceNotFoundException
33 import org.onap.cps.spi.model.DataNode
34 import org.onap.cps.spi.model.DataNodeBuilder
35 import org.springframework.beans.factory.annotation.Autowired
36 import org.springframework.test.context.jdbc.Sql
37
38 import javax.validation.ConstraintViolationException
39
40 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
41 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
42
43 class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
44
45     @Autowired
46     CpsDataPersistenceService objectUnderTest
47
48     static final Gson GSON = new GsonBuilder().create()
49
50     static final String SET_DATA = '/data/fragment.sql'
51     static final long ID_DATA_NODE_WITH_DESCENDANTS = 4001
52     static final String XPATH_DATA_NODE_WITH_DESCENDANTS = '/parent-1'
53     static final String XPATH_DATA_NODE_WITH_LEAVES = '/parent-100'
54     static final long UPDATE_DATA_NODE_FRAGMENT_ID = 4202L
55     static final long UPDATE_DATA_NODE_SUB_FRAGMENT_ID = 4203L
56     static final long LIST_DATA_NODE_PARENT201_FRAGMENT_ID = 4206L
57     static final long LIST_DATA_NODE_PARENT203_FRAGMENT_ID = 4214L
58     static final long LIST_DATA_NODE_CHILD202_FRAGMENT_ID = 4204L
59     static final long LIST_DATA_NODE_PARENT202_FRAGMENT_ID = 4211L
60
61     static final DataNode newDataNode = new DataNodeBuilder().build()
62     static DataNode existingDataNode
63     static DataNode existingChildDataNode
64
65     def expectedLeavesByXpathMap = [
66             '/parent-100'                      : ['parent-leaf': 'parent-leaf value'],
67             '/parent-100/child-001'            : ['first-child-leaf': 'first-child-leaf value'],
68             '/parent-100/child-002'            : ['second-child-leaf': 'second-child-leaf value'],
69             '/parent-100/child-002/grand-child': ['grand-child-leaf': 'grand-child-leaf value']
70     ]
71
72     static {
73         existingDataNode = createDataNodeTree(XPATH_DATA_NODE_WITH_DESCENDANTS)
74         existingChildDataNode = createDataNodeTree('/parent-1/child-1')
75     }
76
77     @Sql([CLEAR_DATA, SET_DATA])
78     def 'StoreDataNode with descendants.'() {
79         when: 'a fragment with descendants is stored'
80             def parentXpath = "/parent-new"
81             def childXpath = "/parent-new/child-new"
82             def grandChildXpath = "/parent-new/child-new/grandchild-new"
83             objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1,
84                     createDataNodeTree(parentXpath, childXpath, grandChildXpath))
85         then: 'it can be retrieved by its xpath'
86             def parentFragment = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME1, parentXpath)
87         and: 'it contains the children'
88             parentFragment.childFragments.size() == 1
89             def childFragment = parentFragment.childFragments[0]
90             childFragment.xpath == childXpath
91         and: "and its children's children"
92             childFragment.childFragments.size() == 1
93             def grandchildFragment = childFragment.childFragments[0]
94             grandchildFragment.xpath == grandChildXpath
95     }
96
97     @Sql([CLEAR_DATA, SET_DATA])
98     def 'Store data node for multiple anchors using the same schema.'() {
99         def xpath = "/parent-new"
100         given: 'a fragment is stored for an anchor'
101             objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1, createDataNodeTree(xpath))
102         when: 'another fragment is stored for an other anchor, using the same schema set'
103             objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME3, createDataNodeTree(xpath))
104         then: 'both fragments can be retrieved by their xpath'
105             def fragment1 = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME1, xpath)
106             fragment1.anchor.name == ANCHOR_NAME1
107             fragment1.xpath == xpath
108             def fragment2 = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME3, xpath)
109             fragment2.anchor.name == ANCHOR_NAME3
110             fragment2.xpath == xpath
111     }
112
113     @Sql([CLEAR_DATA, SET_DATA])
114     def 'Store datanode error scenario: #scenario.'() {
115         when: 'attempt to store a data node with #scenario'
116             objectUnderTest.storeDataNode(dataspaceName, anchorName, dataNode)
117         then: 'a #expectedException is thrown'
118             thrown(expectedException)
119         where: 'the following data is used'
120             scenario                    | dataspaceName  | anchorName     | dataNode         || expectedException
121             'dataspace does not exist'  | 'unknown'      | 'not-relevant' | newDataNode      || DataspaceNotFoundException
122             'schema set does not exist' | DATASPACE_NAME | 'unknown'      | newDataNode      || AnchorNotFoundException
123             'anchor already exists'     | DATASPACE_NAME | ANCHOR_NAME1   | newDataNode      || ConstraintViolationException
124             'datanode already exists'   | DATASPACE_NAME | ANCHOR_NAME1   | existingDataNode || AlreadyDefinedException
125     }
126
127     @Sql([CLEAR_DATA, SET_DATA])
128     def 'Add a child to a Fragment that already has a child.'() {
129         given: ' a new child node'
130             def newChild = createDataNodeTree('xpath for new child')
131         when: 'the child is added to an existing parent with 1 child'
132             objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, newChild)
133         then: 'the parent is now has to 2 children'
134             def expectedExistingChildPath = '/parent-1/child-1'
135             def parentFragment = fragmentRepository.findById(ID_DATA_NODE_WITH_DESCENDANTS).orElseThrow()
136             parentFragment.getChildFragments().size() == 2
137         and: 'it still has the old child'
138             parentFragment.getChildFragments().find({ it.xpath == expectedExistingChildPath })
139         and: 'it has the new child'
140             parentFragment.getChildFragments().find({ it.xpath == newChild.xpath })
141     }
142
143     @Sql([CLEAR_DATA, SET_DATA])
144     def 'Add child error scenario: #scenario.'() {
145         when: 'attempt to add a child data node with #scenario'
146             objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, dataNode)
147         then: 'a #expectedException is thrown'
148             thrown(expectedException)
149         where: 'the following data is used'
150             scenario                 | parentXpath                      | dataNode              || expectedException
151             'parent does not exist'  | 'unknown'                        | newDataNode           || DataNodeNotFoundException
152             'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNode || AlreadyDefinedException
153     }
154
155     @Sql([CLEAR_DATA, SET_DATA])
156     def 'Add list-node fragment with multiple elements including an element with a child datanode.'() {
157         given: 'two new data nodes for an existing list'
158             def listNodeXpaths = ['/parent-201/child-204[@key="B"]', '/parent-201/child-204[@key="C"]']
159             def listNodeCollection = buildDataNodeCollection(listNodeXpaths)
160         and: 'a child node for one of the new data nodes'
161             def childDataNode = buildDataNode('/parent-201/child-204[@key="C"]/grand-child-204[@key2="Z"]', [leave:'value'], [])
162             listNodeCollection.iterator().next().childDataNodes = [childDataNode]
163         when: 'the data nodes (list elements) are added to existing parent node'
164             objectUnderTest.addListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, '/parent-201', listNodeCollection)
165         then: 'new entries successfully persisted, parent node now contains 5 children (2 new + 3 existing before)'
166             def parentFragment = fragmentRepository.getById(LIST_DATA_NODE_PARENT201_FRAGMENT_ID)
167             def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
168             assert allChildXpaths.size() == 5
169             assert allChildXpaths.containsAll(listNodeXpaths)
170         and: 'the child node of the new list entry is also present'
171             def dataspaceEntity = dataspaceRepository.getByName(DATASPACE_NAME)
172             def anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, ANCHOR_NAME3)
173             def listElementChild = fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, childDataNode.xpath)
174             assert listElementChild.isPresent()
175     }
176
177     @Sql([CLEAR_DATA, SET_DATA])
178     def 'Add list-node fragment error scenario: #scenario.'() {
179         given: 'list node data fragment as a collection of data nodes'
180             def listNodeCollection = buildDataNodeCollection(listNodeXpaths)
181         when: 'list-node elements added to existing parent node'
182             objectUnderTest.addListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, parentNodeXpath, listNodeCollection)
183         then: 'a #expectedException is thrown'
184             thrown(expectedException)
185         where: 'following parameters were used'
186             scenario                     | parentNodeXpath | listNodeXpaths                      || expectedException
187             'parent node does not exist' | '/unknown'      | ['irrelevant']                      || DataNodeNotFoundException
188             'already existing fragment'  | '/parent-201'   | ['/parent-201/child-204[@key="A"]'] || AlreadyDefinedException
189
190     }
191
192     static def createDataNodeTree(String... xpaths) {
193         def dataNodeBuilder = new DataNodeBuilder().withXpath(xpaths[0])
194         if (xpaths.length > 1) {
195             def xPathsDescendant = Arrays.copyOfRange(xpaths, 1, xpaths.length)
196             def childDataNode = createDataNodeTree(xPathsDescendant)
197             dataNodeBuilder.withChildDataNodes(ImmutableSet.of(childDataNode))
198         }
199         dataNodeBuilder.build()
200     }
201
202     def getFragmentByXpath(dataspaceName, anchorName, xpath) {
203         def dataspace = dataspaceRepository.getByName(dataspaceName)
204         def anchor = anchorRepository.getByDataspaceAndName(dataspace, anchorName)
205         return fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspace, anchor, xpath).orElseThrow()
206     }
207
208     @Sql([CLEAR_DATA, SET_DATA])
209     def 'Get data node by xpath without descendants.'() {
210         when: 'data node is requested'
211             def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
212                     inputXPath, OMIT_DESCENDANTS)
213         then: 'data node is returned with no descendants'
214             assert result.getXpath() == XPATH_DATA_NODE_WITH_LEAVES
215         and: 'expected leaves'
216             assert result.getChildDataNodes().size() == 0
217             assertLeavesMaps(result.getLeaves(), expectedLeavesByXpathMap[XPATH_DATA_NODE_WITH_LEAVES])
218         where: 'the following data is used'
219             scenario      | inputXPath
220             'some xpath'  | '/parent-100'
221             'root xpath'  | '/'
222             'empty xpath' | ''
223     }
224
225     @Sql([CLEAR_DATA, SET_DATA])
226     def 'Get data node by xpath with all descendants.'() {
227         when: 'data node is requested with all descendants'
228             def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
229                     inputXPath, INCLUDE_ALL_DESCENDANTS)
230             def mappedResult = treeToFlatMapByXpath(new HashMap<>(), result)
231         then: 'data node is returned with all the descendants populated'
232             assert mappedResult.size() == 4
233             assert result.getChildDataNodes().size() == 2
234             assert mappedResult.get('/parent-100/child-001').getChildDataNodes().size() == 0
235             assert mappedResult.get('/parent-100/child-002').getChildDataNodes().size() == 1
236         and: 'extracted leaves maps are matching expected'
237             mappedResult.forEach(
238                     (xPath, dataNode) -> assertLeavesMaps(dataNode.getLeaves(), expectedLeavesByXpathMap[xPath]))
239         where: 'the following data is used'
240             scenario      | inputXPath
241             'some xpath'  | '/parent-100'
242             'root xpath'  | '/'
243             'empty xpath' | ''
244     }
245
246     @Sql([CLEAR_DATA, SET_DATA])
247     def 'Get data node error scenario: #scenario.'() {
248         when: 'attempt to get data node with #scenario'
249             objectUnderTest.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS)
250         then: 'a #expectedException is thrown'
251             thrown(expectedException)
252         where: 'the following data is used'
253             scenario                 | dataspaceName  | anchorName                        | xpath          || expectedException
254             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | 'not relevant' || DataspaceNotFoundException
255             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | 'not relevant' || AnchorNotFoundException
256             'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NO XPATH'     || DataNodeNotFoundException
257     }
258
259     @Sql([CLEAR_DATA, SET_DATA])
260     def 'Update data node leaves.'() {
261         when: 'update is performed for leaves'
262             objectUnderTest.updateDataLeaves(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
263                     "/parent-200/child-201", ['leaf-value': 'new'])
264         then: 'leaves are updated for selected data node'
265             def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
266             def updatedLeaves = getLeavesMap(updatedFragment)
267             assert updatedLeaves.size() == 1
268             assert updatedLeaves.'leaf-value' == 'new'
269         and: 'existing child entry remains as is'
270             def childFragment = updatedFragment.getChildFragments().iterator().next()
271             def childLeaves = getLeavesMap(childFragment)
272             assert childFragment.getId() == UPDATE_DATA_NODE_SUB_FRAGMENT_ID
273             assert childLeaves.'leaf-value' == 'original'
274     }
275
276     @Sql([CLEAR_DATA, SET_DATA])
277     def 'Update data leaves error scenario: #scenario.'() {
278         when: 'attempt to update data node for #scenario'
279             objectUnderTest.updateDataLeaves(dataspaceName, anchorName, xpath, ['leaf-name': 'leaf-value'])
280         then: 'a #expectedException is thrown'
281             thrown(expectedException)
282         where: 'the following data is used'
283             scenario                 | dataspaceName  | anchorName                        | xpath                || expectedException
284             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | 'not relevant'       || DataspaceNotFoundException
285             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | 'not relevant'       || AnchorNotFoundException
286             'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
287     }
288
289     @Sql([CLEAR_DATA, SET_DATA])
290     def 'Replace data node tree with descendants removal.'() {
291         given: 'data node object with leaves updated, no children'
292             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [])
293         when: 'replace data node tree is performed'
294             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
295         then: 'leaves have been updated for selected data node'
296             def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
297             def updatedLeaves = getLeavesMap(updatedFragment)
298             assert updatedLeaves.size() == 1
299             assert updatedLeaves.'leaf-value' == 'new'
300         and: 'updated entry has no children'
301             updatedFragment.getChildFragments().isEmpty()
302         and: 'previously attached child entry is removed from database'
303             fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty()
304     }
305
306     @Sql([CLEAR_DATA, SET_DATA])
307     def 'Replace data node tree with descendants.'() {
308         given: 'data node object with leaves updated, having child with old content'
309             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
310                   buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'original'], [])
311             ])
312         when: 'update is performed including descendants'
313             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
314         then: 'leaves have been updated for selected data node'
315             def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
316             def updatedLeaves = getLeavesMap(updatedFragment)
317             assert updatedLeaves.size() == 1
318             assert updatedLeaves.'leaf-value' == 'new'
319         and: 'existing child entry is not updated as content is same'
320             def childFragment = updatedFragment.getChildFragments().iterator().next()
321             childFragment.getXpath() == '/parent-200/child-201/grand-child'
322             def childLeaves = getLeavesMap(childFragment)
323             assert childLeaves.'leaf-value' == 'original'
324     }
325
326     @Sql([CLEAR_DATA, SET_DATA])
327     def 'Replace data node tree with same descendants but changed leaf value.'() {
328         given: 'data node object with leaves updated, having child with old content'
329             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
330                     buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'new'], [])
331             ])
332         when: 'update is performed including descendants'
333             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
334         then: 'leaves have been updated for selected data node'
335             def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
336             def updatedLeaves = getLeavesMap(updatedFragment)
337             assert updatedLeaves.size() == 1
338             assert updatedLeaves.'leaf-value' == 'new'
339         and: 'existing child entry is updated with the new content'
340             def childFragment = updatedFragment.getChildFragments().iterator().next()
341             childFragment.getXpath() == '/parent-200/child-201/grand-child'
342             def childLeaves = getLeavesMap(childFragment)
343             assert childLeaves.'leaf-value' == 'new'
344     }
345
346     @Sql([CLEAR_DATA, SET_DATA])
347     def 'Replace data node tree with different descendants xpath'() {
348         given: 'data node object with leaves updated, having child with old content'
349             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
350                     buildDataNode("/parent-200/child-201/grand-child-new", ['leaf-value': 'new'], [])
351             ])
352         when: 'update is performed including descendants'
353             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
354         then: 'leaves have been updated for selected data node'
355             def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
356             def updatedLeaves = getLeavesMap(updatedFragment)
357             assert updatedLeaves.size() == 1
358             assert updatedLeaves.'leaf-value' == 'new'
359         and: 'previously attached child entry is removed from database'
360             fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty()
361         and: 'new child entry is persisted'
362             def childFragment = updatedFragment.getChildFragments().iterator().next()
363             childFragment.getXpath() == '/parent-200/child-201/grand-child-new'
364             def childLeaves = getLeavesMap(childFragment)
365             assert childLeaves.'leaf-value' == 'new'
366     }
367
368     @Sql([CLEAR_DATA, SET_DATA])
369     def 'Replace data node tree error scenario: #scenario.'() {
370         given: 'data node object'
371             def submittedDataNode = buildDataNode(xpath, ['leaf-name': 'leaf-value'], [])
372         when: 'attempt to update data node for #scenario'
373             objectUnderTest.replaceDataNodeTree(dataspaceName, anchorName, submittedDataNode)
374         then: 'a #expectedException is thrown'
375             thrown(expectedException)
376         where: 'the following data is used'
377             scenario                 | dataspaceName  | anchorName                        | xpath                || expectedException
378             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | 'not relevant'       || DataspaceNotFoundException
379             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | 'not relevant'       || AnchorNotFoundException
380             'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
381     }
382
383     @Sql([CLEAR_DATA, SET_DATA])
384     def 'Replace list-node content of #scenario.'() {
385         given: 'list node data fragment as a collection of data nodes'
386             def listNodeCollection = buildDataNodeCollection(listNodeXpaths)
387         when: 'list-node elements replaced within the existing parent node'
388             objectUnderTest.replaceListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, '/parent-201', listNodeCollection)
389         then: 'child list elements are updated as expected, non-list element remains as is'
390             def parentFragment = fragmentRepository.getById(LIST_DATA_NODE_PARENT201_FRAGMENT_ID)
391             def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
392             assert allChildXpaths.size() == expectedChildXpaths.size()
393             assert allChildXpaths.containsAll(expectedChildXpaths)
394         where: 'following parameters were used'
395             scenario                 | listNodeXpaths                      || expectedChildXpaths
396             'existing list-node'     | ['/parent-201/child-204[@key="B"]'] || ['/parent-201/child-203', '/parent-201/child-204[@key="B"]']
397             '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"]']
398     }
399
400     @Sql([CLEAR_DATA, SET_DATA])
401     def 'Replace list-node fragment error scenario: #scenario.'() {
402         given: 'list node data fragment as a collection of data nodes'
403             def listNodeCollection = buildDataNodeCollection(listNodeXpaths)
404         when: 'list-node elements were replaced under existing parent node'
405             objectUnderTest.replaceListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, parentNodeXpath, listNodeCollection)
406         then: 'a #expectedException is thrown'
407             thrown(expectedException)
408         where: 'following parameters were used'
409             scenario                     | parentNodeXpath | listNodeXpaths || expectedException
410             'parent node does not exist' | '/unknown'      | ['irrelevant'] || DataNodeNotFoundException
411     }
412
413     @Sql([CLEAR_DATA, SET_DATA])
414     def 'Delete list-node content of #scenario.'() {
415         given: 'list node data fragments are present in database'
416         when: 'list-node elements deleted within the existing parent node'
417             objectUnderTest.deleteListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, listNodeXpaths)
418         then: 'child list elements are removed as expected, non-list element remains as is'
419             def parentFragment = fragmentRepository.getById(listNodeFragmentID)
420             def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
421             assert allChildXpaths.size() == expectedChildXpaths.size()
422             assert allChildXpaths.containsAll(expectedChildXpaths)
423         where: 'following parameters were used'
424             scenario                                          | listNodeXpaths                                               | listNodeFragmentID                   || expectedChildXpaths
425             '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"]']
426             '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"]']
427             '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"]']
428             '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"]']
429             'existing node with list node variants to delete' | '/parent-203/child-204'                                      | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203']
430             'existing grandchild list-node'                   | '/parent-200/child-202/grand-child-202[@key="D"]'            | LIST_DATA_NODE_CHILD202_FRAGMENT_ID  || []
431     }
432
433     @Sql([CLEAR_DATA, SET_DATA])
434     def 'Delete list-node fragment error scenario: #scenario.'() {
435         given: 'list node data fragments are present in database'
436         when: 'list-node elements are deleted under existing parent node'
437             objectUnderTest.deleteListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, listNodeXpaths)
438         then: 'a #expectedException is thrown'
439             thrown(expectedException)
440         where: 'following parameters were used'
441             scenario                                            | listNodeXpaths                                    || expectedException
442             'list parent node does not exist'                   | '/unknown/unknown'                                || DataNodeNotFoundException
443             'list child nodes do not exist'                     | '/parent-200/unknown'                             || DataNodeNotFoundException
444             'list child nodes with key does not exist'          | '/parent-200/unknown[@key="C"]'                   || DataNodeNotFoundException
445             'list grandchild nodes parent does not exist'       | '/parent-200/unknown/unknown'                     || DataNodeNotFoundException
446             'non-existing parent with existing list-node'       | '/unknown/child-204'                              || DataNodeNotFoundException
447             'non-existing parent with existing list-node & key' | '/unknown/child-204[@key="A"]'                    || DataNodeNotFoundException
448             'valid with non existing key'                       | '/parent-200/child-202/grand-child-202[@key="A"]' || DataNodeNotFoundException
449             'child list node without key'                       | '/parent-200/child-204/grand-child-204'           || DataNodeNotFoundException
450             'valid list node with invalid key'                  | '/parent-203/child-204[@key="C"]'                 || DataNodeNotFoundException
451
452     }
453
454     static Collection<DataNode> buildDataNodeCollection(xpaths) {
455         return xpaths.collect { new DataNodeBuilder().withXpath(it).build() }
456     }
457
458     static DataNode buildDataNode(xpath, leaves, childDataNodes) {
459         return new DataNodeBuilder().withXpath(xpath).withLeaves(leaves).withChildDataNodes(childDataNodes).build()
460     }
461
462     static Map<String, Object> getLeavesMap(FragmentEntity fragmentEntity) {
463         return GSON.fromJson(fragmentEntity.getAttributes(), Map<String, Object>.class)
464     }
465
466     def static assertLeavesMaps(actualLeavesMap, expectedLeavesMap) {
467         expectedLeavesMap.forEach((key, value) -> {
468             def actualValue = actualLeavesMap[key]
469             if (value instanceof Collection<?> && actualValue instanceof Collection<?>) {
470                 assert value.size() == actualValue.size()
471                 assert value.containsAll(actualValue)
472             } else {
473                 assert value == actualValue
474             }
475         })
476         return true
477     }
478
479     def static treeToFlatMapByXpath(Map<String, DataNode> flatMap, DataNode dataNodeTree) {
480         flatMap.put(dataNodeTree.getXpath(), dataNodeTree)
481         dataNodeTree.getChildDataNodes()
482                 .forEach(childDataNode -> treeToFlatMapByXpath(flatMap, childDataNode))
483         return flatMap
484     }
485
486 }