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