Test Scenario for replaceListContent
[cps.git] / cps-ri / src / test / groovy / org / onap / cps / spi / impl / CpsDataPersistenceServiceIntegrationSpec.groovy
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2021-2022 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.fasterxml.jackson.databind.ObjectMapper
25 import com.google.common.collect.ImmutableSet
26 import org.onap.cps.spi.CpsDataPersistenceService
27 import org.onap.cps.spi.entities.FragmentEntity
28 import org.onap.cps.spi.exceptions.AlreadyDefinedException
29 import org.onap.cps.spi.exceptions.AnchorNotFoundException
30 import org.onap.cps.spi.exceptions.CpsAdminException
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.onap.cps.utils.JsonObjectMapper
36 import org.springframework.beans.factory.annotation.Autowired
37 import org.springframework.test.context.jdbc.Sql
38
39 import javax.validation.ConstraintViolationException
40
41 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
42 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
43
44 class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
45
46     @Autowired
47     CpsDataPersistenceService objectUnderTest
48
49     static final JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
50
51     static final String SET_DATA = '/data/fragment.sql'
52     static final long ID_DATA_NODE_WITH_DESCENDANTS = 4001
53     static final String XPATH_DATA_NODE_WITH_DESCENDANTS = '/parent-1'
54     static final String XPATH_DATA_NODE_WITH_LEAVES = '/parent-100'
55     static final long UPDATE_DATA_NODE_FRAGMENT_ID = 4202L
56     static final long UPDATE_DATA_NODE_SUB_FRAGMENT_ID = 4203L
57     static final long LIST_DATA_NODE_PARENT201_FRAGMENT_ID = 4206L
58     static final long LIST_DATA_NODE_PARENT203_FRAGMENT_ID = 4214L
59     static final long LIST_DATA_NODE_PARENT202_FRAGMENT_ID = 4211L
60     static final long PARENT_3_FRAGMENT_ID = 4003L
61
62     static final DataNode newDataNode = new DataNodeBuilder().build()
63     static DataNode existingDataNode
64     static DataNode existingChildDataNode
65
66     def expectedLeavesByXpathMap = [
67             '/parent-100'                      : ['parent-leaf': 'parent-leaf value'],
68             '/parent-100/child-001'            : ['first-child-leaf': 'first-child-leaf value'],
69             '/parent-100/child-002'            : ['second-child-leaf': 'second-child-leaf value'],
70             '/parent-100/child-002/grand-child': ['grand-child-leaf': 'grand-child-leaf value']
71     ]
72
73     static {
74         existingDataNode = createDataNodeTree(XPATH_DATA_NODE_WITH_DESCENDANTS)
75         existingChildDataNode = createDataNodeTree('/parent-1/child-1')
76     }
77
78     @Sql([CLEAR_DATA, SET_DATA])
79     def 'StoreDataNode with descendants.'() {
80         when: 'a fragment with descendants is stored'
81             def parentXpath = "/parent-new"
82             def childXpath = "/parent-new/child-new"
83             def grandChildXpath = "/parent-new/child-new/grandchild-new"
84             objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1,
85                     createDataNodeTree(parentXpath, childXpath, grandChildXpath))
86         then: 'it can be retrieved by its xpath'
87             def parentFragment = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME1, parentXpath)
88         and: 'it contains the children'
89             parentFragment.childFragments.size() == 1
90             def childFragment = parentFragment.childFragments[0]
91             childFragment.xpath == childXpath
92         and: "and its children's children"
93             childFragment.childFragments.size() == 1
94             def grandchildFragment = childFragment.childFragments[0]
95             grandchildFragment.xpath == grandChildXpath
96     }
97
98     @Sql([CLEAR_DATA, SET_DATA])
99     def 'Store data node for multiple anchors using the same schema.'() {
100         def xpath = "/parent-new"
101         given: 'a fragment is stored for an anchor'
102             objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1, createDataNodeTree(xpath))
103         when: 'another fragment is stored for an other anchor, using the same schema set'
104             objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME3, createDataNodeTree(xpath))
105         then: 'both fragments can be retrieved by their xpath'
106             def fragment1 = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME1, xpath)
107             fragment1.anchor.name == ANCHOR_NAME1
108             fragment1.xpath == xpath
109             def fragment2 = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME3, xpath)
110             fragment2.anchor.name == ANCHOR_NAME3
111             fragment2.xpath == xpath
112     }
113
114     @Sql([CLEAR_DATA, SET_DATA])
115     def 'Store datanode error scenario: #scenario.'() {
116         when: 'attempt to store a data node with #scenario'
117             objectUnderTest.storeDataNode(dataspaceName, anchorName, dataNode)
118         then: 'a #expectedException is thrown'
119             thrown(expectedException)
120         where: 'the following data is used'
121             scenario                    | dataspaceName  | anchorName     | dataNode         || expectedException
122             'dataspace does not exist'  | 'unknown'      | 'not-relevant' | newDataNode      || DataspaceNotFoundException
123             'schema set does not exist' | DATASPACE_NAME | 'unknown'      | newDataNode      || AnchorNotFoundException
124             'anchor already exists'     | DATASPACE_NAME | ANCHOR_NAME1   | newDataNode      || ConstraintViolationException
125             'datanode already exists'   | DATASPACE_NAME | ANCHOR_NAME1   | existingDataNode || AlreadyDefinedException
126     }
127
128     @Sql([CLEAR_DATA, SET_DATA])
129     def 'Add a child to a Fragment that already has a child.'() {
130         given: ' a new child node'
131             def newChild = createDataNodeTree('xpath for new child')
132         when: 'the child is added to an existing parent with 1 child'
133             objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, newChild)
134         then: 'the parent is now has to 2 children'
135             def expectedExistingChildPath = '/parent-1/child-1'
136             def parentFragment = fragmentRepository.findById(ID_DATA_NODE_WITH_DESCENDANTS).orElseThrow()
137             parentFragment.childFragments.size() == 2
138         and: 'it still has the old child'
139             parentFragment.childFragments.find({ it.xpath == expectedExistingChildPath })
140         and: 'it has the new child'
141             parentFragment.childFragments.find({ it.xpath == newChild.xpath })
142     }
143
144     @Sql([CLEAR_DATA, SET_DATA])
145     def 'Add child error scenario: #scenario.'() {
146         when: 'attempt to add a child data node with #scenario'
147             objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, dataNode)
148         then: 'a #expectedException is thrown'
149             thrown(expectedException)
150         where: 'the following data is used'
151             scenario                 | parentXpath                      | dataNode              || expectedException
152             'parent does not exist'  | 'unknown'                        | newDataNode           || DataNodeNotFoundException
153             'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNode || AlreadyDefinedException
154     }
155
156     @Sql([CLEAR_DATA, SET_DATA])
157     def 'Add multiple list elements including an element with a child datanode.'() {
158         given: 'two new data nodes for an existing list'
159             def listElementXpaths = ['/parent-201/child-204[@key="NEW1"]', '/parent-201/child-204[@key="NEW2"]']
160             def listElements = toDataNodes(listElementXpaths)
161         and: 'a child node for one of the new data nodes'
162             def childDataNode = buildDataNode('/parent-201/child-204[@key="NEW1"]/grand-child-204[@key2="NEW1-CHILD"]', [leave:'value'], [])
163             listElements[0].childDataNodes = [childDataNode]
164         when: 'the data nodes (list elements) are added to existing parent node'
165             objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME3, '/parent-201', listElements)
166         then: 'new entries successfully persisted, parent node now contains 5 children (2 new + 3 existing before)'
167             def parentFragment = fragmentRepository.getById(LIST_DATA_NODE_PARENT201_FRAGMENT_ID)
168             def allChildXpaths = parentFragment.childFragments.collect { it.xpath }
169             assert allChildXpaths.size() == 5
170             assert allChildXpaths.containsAll(listElementXpaths)
171         and: 'the child node of the new list entry is also present'
172             def dataspaceEntity = dataspaceRepository.getByName(DATASPACE_NAME)
173             def anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, ANCHOR_NAME3)
174             def listElementChild = fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, childDataNode.xpath)
175             assert listElementChild.isPresent()
176     }
177
178     @Sql([CLEAR_DATA, SET_DATA])
179     def 'Add list element error scenario: #scenario.'() {
180         given: 'list element as a collection of data nodes'
181             def listElementCollection = toDataNodes(listElementXpaths)
182         when: 'attempt to add list elements to parent node'
183             objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME3, parentNodeXpath, listElementCollection)
184         then: 'a #expectedException is thrown'
185             thrown(expectedException)
186         where: 'following parameters were used'
187             scenario                     | parentNodeXpath | listElementXpaths                   || expectedException
188             'parent node does not exist' | '/unknown'      | ['irrelevant']                      || DataNodeNotFoundException
189             'already existing fragment'  | '/parent-201'   | ['/parent-201/child-204[@key="A"]'] || AlreadyDefinedException
190     }
191
192     @Sql([CLEAR_DATA, SET_DATA])
193     def 'Get data node by xpath without descendants.'() {
194         when: 'data node is requested'
195             def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
196                     inputXPath, OMIT_DESCENDANTS)
197         then: 'data node is returned with no descendants'
198             assert result.xpath == XPATH_DATA_NODE_WITH_LEAVES
199         and: 'expected leaves'
200             assert result.childDataNodes.size() == 0
201             assertLeavesMaps(result.leaves, expectedLeavesByXpathMap[XPATH_DATA_NODE_WITH_LEAVES])
202         where: 'the following data is used'
203             scenario      | inputXPath
204             'some xpath'  | '/parent-100'
205             'root xpath'  | '/'
206             'empty xpath' | ''
207     }
208
209     @Sql([CLEAR_DATA, SET_DATA])
210     def 'Get data node by xpath with all descendants.'() {
211         when: 'data node is requested with all descendants'
212             def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
213                     inputXPath, INCLUDE_ALL_DESCENDANTS)
214             def mappedResult = treeToFlatMapByXpath(new HashMap<>(), result)
215         then: 'data node is returned with all the descendants populated'
216             assert mappedResult.size() == 4
217             assert result.childDataNodes.size() == 2
218             assert mappedResult.get('/parent-100/child-001').childDataNodes.size() == 0
219             assert mappedResult.get('/parent-100/child-002').childDataNodes.size() == 1
220         and: 'extracted leaves maps are matching expected'
221             mappedResult.forEach(
222                     (xPath, dataNode) -> assertLeavesMaps(dataNode.leaves, expectedLeavesByXpathMap[xPath]))
223         where: 'the following data is used'
224             scenario      | inputXPath
225             'some xpath'  | '/parent-100'
226             'root xpath'  | '/'
227             'empty xpath' | ''
228     }
229
230     @Sql([CLEAR_DATA, SET_DATA])
231     def 'Get data node error scenario: #scenario.'() {
232         when: 'attempt to get data node with #scenario'
233             objectUnderTest.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS)
234         then: 'a #expectedException is thrown'
235             thrown(expectedException)
236         where: 'the following data is used'
237             scenario                 | dataspaceName  | anchorName                        | xpath          || expectedException
238             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | 'not relevant' || DataspaceNotFoundException
239             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | 'not relevant' || AnchorNotFoundException
240             'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NO XPATH'     || DataNodeNotFoundException
241     }
242
243     @Sql([CLEAR_DATA, SET_DATA])
244     def 'Update data node leaves.'() {
245         when: 'update is performed for leaves'
246             objectUnderTest.updateDataLeaves(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
247                     "/parent-200/child-201", ['leaf-value': 'new'])
248         then: 'leaves are updated for selected data node'
249             def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
250             def updatedLeaves = getLeavesMap(updatedFragment)
251             assert updatedLeaves.size() == 1
252             assert updatedLeaves.'leaf-value' == 'new'
253         and: 'existing child entry remains as is'
254             def childFragment = updatedFragment.childFragments.iterator().next()
255             def childLeaves = getLeavesMap(childFragment)
256             assert childFragment.id == UPDATE_DATA_NODE_SUB_FRAGMENT_ID
257             assert childLeaves.'leaf-value' == 'original'
258     }
259
260     @Sql([CLEAR_DATA, SET_DATA])
261     def 'Update data leaves error scenario: #scenario.'() {
262         when: 'attempt to update data node for #scenario'
263             objectUnderTest.updateDataLeaves(dataspaceName, anchorName, xpath, ['leaf-name': 'leaf-value'])
264         then: 'a #expectedException is thrown'
265             thrown(expectedException)
266         where: 'the following data is used'
267             scenario                 | dataspaceName  | anchorName                        | xpath                || expectedException
268             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | 'not relevant'       || DataspaceNotFoundException
269             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | 'not relevant'       || AnchorNotFoundException
270             'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
271     }
272
273     @Sql([CLEAR_DATA, SET_DATA])
274     def 'Replace data node tree with descendants removal.'() {
275         given: 'data node object with leaves updated, no children'
276             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [])
277         when: 'replace data node tree is performed'
278             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
279         then: 'leaves have been updated for selected data node'
280             def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
281             def updatedLeaves = getLeavesMap(updatedFragment)
282             assert updatedLeaves.size() == 1
283             assert updatedLeaves.'leaf-value' == 'new'
284         and: 'updated entry has no children'
285             updatedFragment.childFragments.isEmpty()
286         and: 'previously attached child entry is removed from database'
287             fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty()
288     }
289
290     @Sql([CLEAR_DATA, SET_DATA])
291     def 'Replace data node tree with descendants.'() {
292         given: 'data node object with leaves updated, having child with old content'
293             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
294                   buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'original'], [])
295             ])
296         when: 'update is performed including descendants'
297             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
298         then: 'leaves have been updated for selected data node'
299             def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
300             def updatedLeaves = getLeavesMap(updatedFragment)
301             assert updatedLeaves.size() == 1
302             assert updatedLeaves.'leaf-value' == 'new'
303         and: 'existing child entry is not updated as content is same'
304             def childFragment = updatedFragment.childFragments.iterator().next()
305             childFragment.xpath == '/parent-200/child-201/grand-child'
306             def childLeaves = getLeavesMap(childFragment)
307             assert childLeaves.'leaf-value' == 'original'
308     }
309
310     @Sql([CLEAR_DATA, SET_DATA])
311     def 'Replace data node tree with same descendants but changed leaf value.'() {
312         given: 'data node object with leaves updated, having child with old content'
313             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
314                     buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'new'], [])
315             ])
316         when: 'update is performed including descendants'
317             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
318         then: 'leaves have been updated for selected data node'
319             def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
320             def updatedLeaves = getLeavesMap(updatedFragment)
321             assert updatedLeaves.size() == 1
322             assert updatedLeaves.'leaf-value' == 'new'
323         and: 'existing child entry is updated with the new content'
324             def childFragment = updatedFragment.childFragments.iterator().next()
325             childFragment.xpath == '/parent-200/child-201/grand-child'
326             def childLeaves = getLeavesMap(childFragment)
327             assert childLeaves.'leaf-value' == 'new'
328     }
329
330     @Sql([CLEAR_DATA, SET_DATA])
331     def 'Replace data node tree with different descendants xpath'() {
332         given: 'data node object with leaves updated, having child with old content'
333             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
334                     buildDataNode("/parent-200/child-201/grand-child-new", ['leaf-value': 'new'], [])
335             ])
336         when: 'update is performed including descendants'
337             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
338         then: 'leaves have been updated for selected data node'
339             def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
340             def updatedLeaves = getLeavesMap(updatedFragment)
341             assert updatedLeaves.size() == 1
342             assert updatedLeaves.'leaf-value' == 'new'
343         and: 'previously attached child entry is removed from database'
344             fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty()
345         and: 'new child entry is persisted'
346             def childFragment = updatedFragment.childFragments.iterator().next()
347             childFragment.xpath == '/parent-200/child-201/grand-child-new'
348             def childLeaves = getLeavesMap(childFragment)
349             assert childLeaves.'leaf-value' == 'new'
350     }
351
352     @Sql([CLEAR_DATA, SET_DATA])
353     def 'Replace data node tree error scenario: #scenario.'() {
354         given: 'data node object'
355             def submittedDataNode = buildDataNode(xpath, ['leaf-name': 'leaf-value'], [])
356         when: 'attempt to update data node for #scenario'
357             objectUnderTest.replaceDataNodeTree(dataspaceName, anchorName, submittedDataNode)
358         then: 'a #expectedException is thrown'
359             thrown(expectedException)
360         where: 'the following data is used'
361             scenario                 | dataspaceName  | anchorName                        | xpath                || expectedException
362             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | 'not relevant'       || DataspaceNotFoundException
363             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | 'not relevant'       || AnchorNotFoundException
364             'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
365     }
366
367     @Sql([CLEAR_DATA, SET_DATA])
368     def 'Update existing list with #scenario.'() {
369         given: 'a parent having a list of data nodes containing: #originalKeys (ech list element has a child too)'
370             def parentXpath = '/parent-3'
371             if (originalKeys.size() > 0) {
372                 def originalListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'original value', originalKeys, true)
373                 objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, originalListEntriesAsDataNodes)
374             }
375         and: 'each original list element has one child'
376             def originalParentFragment = fragmentRepository.getById(PARENT_3_FRAGMENT_ID)
377             originalParentFragment.childFragments.each {assert it.childFragments.size() == 1 }
378         when: 'it is updated with #scenario'
379             def replacementListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'new value', replacementKeys, false)
380             objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, replacementListEntriesAsDataNodes)
381         then: 'the result list ONLY contains the expected replacement elements'
382             def parentFragment = fragmentRepository.getById(PARENT_3_FRAGMENT_ID)
383             def allChildXpaths = parentFragment.childFragments.collect { it.xpath }
384             def expectedListEntriesAfterUpdateAsXpaths = keysToXpaths(parentXpath, replacementKeys)
385             assert allChildXpaths.size() == replacementKeys.size()
386             assert allChildXpaths.containsAll(expectedListEntriesAfterUpdateAsXpaths)
387         and: 'all the list elements have the new values'
388             assert parentFragment.childFragments.stream().allMatch(childFragment -> childFragment.attributes.contains('new value'))
389         and: 'there are no more grandchildren as none of the replacement list entries had a child'
390             parentFragment.childFragments.each {assert it.childFragments.size() == 0 }
391         where: 'the following replacement lists are applied'
392             scenario                                            | originalKeys | replacementKeys
393             'one existing entry only'                           | []           | ['NEW']
394             'multiple new entries'                              | []           | ['NEW1', 'NEW2']
395             'one new entry only (existing entries are deleted)' | ['A', 'B']   | ['NEW1', 'NEW2']
396             'one existing on new entry'                         | ['A', 'B']   | ['A', 'NEW']
397             'one existing entry only'                           | ['A', 'B']   | ['A']
398     }
399
400     @Sql([CLEAR_DATA, SET_DATA])
401     def 'Replacing existing list element with attributes and (grand)child.'() {
402         given: 'a parent with list elements A and B with attribute and grandchild tagged as "org"'
403             def parentXpath = '/parent-3'
404             def originalListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'org', ['A','B'], true)
405             objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, originalListEntriesAsDataNodes)
406         when: 'A is replaced with an entry with attribute and grandchild tagged tagged as "new" (B is not in replacement list)'
407             def replacementListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'new', ['A'], true)
408             objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, replacementListEntriesAsDataNodes)
409         then: 'The updated fragment has a child-list with ONLY element "A"'
410             def parentFragment = fragmentRepository.getById(PARENT_3_FRAGMENT_ID)
411             parentFragment.childFragments.size() == 1
412             def childListElementA = parentFragment.childFragments[0]
413             childListElementA.xpath == "/parent-3/child-list[@key='A']"
414         and: 'element "A" has an attribute with the "new" (tag) value'
415             childListElementA.attributes == '{"attr1": "new"}'
416         and: 'element "A" has a only one (grand)child'
417             childListElementA.childFragments.size() == 1
418         and: 'the grandchild is the new grandchild (tag)'
419             def grandChild = childListElementA.childFragments[0]
420             grandChild.xpath == "/parent-3/child-list[@key='A']/new-grand-child"
421         and: 'the grandchild has an attribute with the "new" (tag) value'
422             grandChild.attributes == '{"attr1": "new"}'
423     }
424
425     @Sql([CLEAR_DATA, SET_DATA])
426     def 'Replace list element for a parent (parent-1) with existing one (non-list) child'() {
427         when: 'a list element is added under the parent'
428             def replacementListEntriesAsDataNodes = createChildListAllHavingAttributeValue(XPATH_DATA_NODE_WITH_DESCENDANTS, 'new', ['A','B'], false)
429             objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, replacementListEntriesAsDataNodes)
430         then: 'the parent will have 3 children after the replacement'
431             def parentFragment = fragmentRepository.getById(ID_DATA_NODE_WITH_DESCENDANTS)
432             parentFragment.childFragments.size() == 3
433             def xpaths = parentFragment.childFragments.collect {it.xpath}
434         and: 'one of the children is the original child fragment'
435             xpaths.contains('/parent-1/child-1')
436         and: 'it has the two new list elements'
437             xpaths.containsAll("/parent-1/child-list[@key='A']", "/parent-1/child-list[@key='B']")
438     }
439
440     @Sql([CLEAR_DATA, SET_DATA])
441     def 'Replace list content using unknown parent'() {
442         given: 'list element as a collection of data nodes'
443             def listElementCollection = toDataNodes(['irrelevant'])
444         when: 'attempt to replace list elements under unknown parent node'
445             objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, '/unknown', listElementCollection)
446         then: 'a datanode not found exception is thrown'
447             thrown(DataNodeNotFoundException)
448     }
449
450     @Sql([CLEAR_DATA, SET_DATA])
451     def 'Replace list content with empty collection is not supported'() {
452         when: 'attempt to replace list elements with empty collection'
453             objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, '/parent-203', [])
454         then: 'a CPS admin exception is thrown'
455             def thrown = thrown(CpsAdminException)
456             assert thrown.message == 'Invalid list replacement'
457     }
458
459     @Sql([CLEAR_DATA, SET_DATA])
460     def 'Delete list scenario: #scenario.'() {
461         when: 'deleting list is executed for: #scenario.'
462             objectUnderTest.deleteListDataNode(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths)
463         then: 'only the expected children remain'
464             def parentFragment = fragmentRepository.getById(parentFragmentId)
465             def remainingChildXpaths = parentFragment.childFragments.collect { it.xpath }
466             assert remainingChildXpaths.size() == expectedRemainingChildXpaths.size()
467             assert remainingChildXpaths.containsAll(expectedRemainingChildXpaths)
468         where: 'following parameters were used'
469             scenario                          | targetXpaths                                                 | parentFragmentId                     || expectedRemainingChildXpaths
470             '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="B"]']
471             '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"]']
472             'whole list'                      | '/parent-203/child-204'                                      | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203']
473             'list element under list element' | '/parent-203/child-204[@key="B"]/grand-child-204[@key2="Y"]' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="A"]', '/parent-203/child-204[@key="B"]']
474     }
475
476     @Sql([CLEAR_DATA, SET_DATA])
477     def 'Delete list error scenario: #scenario.'() {
478         when: 'attempting to delete scenario: #scenario.'
479             objectUnderTest.deleteListDataNode(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths)
480         then: 'a DataNodeNotFoundException is thrown'
481             thrown(DataNodeNotFoundException)
482         where: 'following parameters were used'
483             scenario                                   | targetXpaths
484             'whole list, parent node does not exist'   | '/unknown/some-child'
485             'list element, parent node does not exist' | '/unknown/child-204[@key="A"]'
486             'whole list does not exist'                | '/parent-200/unknown'
487             'list element, list does not exist'        | '/parent-200/unknown[@key="C"]'
488             'list element, element does not exist'     | '/parent-203/child-204[@key="C"]'
489             'valid datanode but not a list'            | '/parent-200/child-202'
490     }
491
492     @Sql([CLEAR_DATA, SET_DATA])
493     def 'Confirm deletion of #scenario.'() {
494         given: 'a valid data node'
495             def dataNode
496             def dataNodeXpath
497         when: 'data nodes are deleted'
498             objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion)
499         then: 'verify data nodes are removed'
500             try {
501                 dataNode = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, getDataNodesXpaths, INCLUDE_ALL_DESCENDANTS)
502                 dataNodeXpath = dataNode.xpath
503                 assert dataNodeXpath == expectedXpaths
504             } catch (DataNodeNotFoundException) {
505                 assert dataNodeXpath == expectedXpaths
506             }
507         where: 'following parameters were used'
508             scenario                                | xpathForDeletion                                   | getDataNodesXpaths                                || expectedXpaths
509             'child of target'                       | '/parent-206/child-206'                            | '/parent-206/child-206'                           || null
510             'child data node, parent still exists'  | '/parent-206/child-206'                            | '/parent-206'                                     || '/parent-206'
511             'list element'                          | '/parent-206/child-206/grand-child-206[@key="A"]'  | '/parent-206/child-206/grand-child-206[@key="A"]' || null
512             '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"]'
513     }
514
515     @Sql([CLEAR_DATA, SET_DATA])
516     def 'Delete data node with #scenario.'() {
517         when: 'data node is deleted'
518             objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, datanodeXpath)
519         then: 'a #expectedException is thrown'
520             thrown(DataNodeNotFoundException)
521         where: 'the following parameters were used'
522             scenario                                        | datanodeXpath
523             'valid data node, non existent child node'      | '/parent-203/child-non-existent'
524             'invalid list element'                          | '/parent-206/child-206/grand-child-206@key="A"]'
525     }
526
527     static Collection<DataNode> toDataNodes(xpaths) {
528         return xpaths.collect { new DataNodeBuilder().withXpath(it).build() }
529     }
530
531     static DataNode buildDataNode(xpath, leaves, childDataNodes) {
532         return new DataNodeBuilder().withXpath(xpath).withLeaves(leaves).withChildDataNodes(childDataNodes).build()
533     }
534
535     static Map<String, Object> getLeavesMap(FragmentEntity fragmentEntity) {
536         return jsonObjectMapper.convertJsonString(fragmentEntity.attributes, Map<String, Object>.class)
537     }
538
539     def static assertLeavesMaps(actualLeavesMap, expectedLeavesMap) {
540         expectedLeavesMap.forEach((key, value) -> {
541             def actualValue = actualLeavesMap[key]
542             if (value instanceof Collection<?> && actualValue instanceof Collection<?>) {
543                 assert value.size() == actualValue.size()
544                 assert value.containsAll(actualValue)
545             } else {
546                 assert value == actualValue
547             }
548         })
549         return true
550     }
551
552     def static treeToFlatMapByXpath(Map<String, DataNode> flatMap, DataNode dataNodeTree) {
553         flatMap.put(dataNodeTree.xpath, dataNodeTree)
554         dataNodeTree.childDataNodes
555                 .forEach(childDataNode -> treeToFlatMapByXpath(flatMap, childDataNode))
556         return flatMap
557     }
558
559     def keysToXpaths(parent, Collection keys) {
560         return keys.collect { "${parent}/child-list[@key='${it}']".toString() }
561     }
562
563     def static createDataNodeTree(String... xpaths) {
564         def dataNodeBuilder = new DataNodeBuilder().withXpath(xpaths[0])
565         if (xpaths.length > 1) {
566             def xPathsDescendant = Arrays.copyOfRange(xpaths, 1, xpaths.length)
567             def childDataNode = createDataNodeTree(xPathsDescendant)
568             dataNodeBuilder.withChildDataNodes(ImmutableSet.of(childDataNode))
569         }
570         dataNodeBuilder.build()
571     }
572
573     def getFragmentByXpath(dataspaceName, anchorName, xpath) {
574         def dataspace = dataspaceRepository.getByName(dataspaceName)
575         def anchor = anchorRepository.getByDataspaceAndName(dataspace, anchorName)
576         return fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspace, anchor, xpath).orElseThrow()
577     }
578
579
580     def createChildListAllHavingAttributeValue(parentXpath, tag, Collection keys, boolean addGrandChild) {
581         def listElementAsDataNodes = keysToXpaths(parentXpath, keys).collect {
582                 new DataNodeBuilder()
583                     .withXpath(it)
584                     .withLeaves([attr1: tag])
585                     .build()
586         }
587         if (addGrandChild) {
588             listElementAsDataNodes.each {it.childDataNodes = [createGrandChild(it.xpath, tag)]}
589         }
590         return listElementAsDataNodes
591     }
592
593     def createGrandChild(parentXPath, tag) {
594         new DataNodeBuilder()
595             .withXpath("${parentXPath}/${tag}-grand-child")
596             .withLeaves([attr1: tag])
597             .build()
598     }
599
600 }