Move integration test (DataService)
[cps.git] / cps-ri / src / test / groovy / org / onap / cps / spi / impl / CpsDataPersistenceServiceIntegrationSpec.groovy
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2021-2023 Nordix Foundation
4  *  Modifications Copyright (C) 2021 Pantheon.tech
5  *  Modifications Copyright (C) 2021-2022 Bell Canada.
6  *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
7  *  ================================================================================
8  *  Licensed under the Apache License, Version 2.0 (the "License");
9  *  you may not use this file except in compliance with the License.
10  *  You may obtain a copy of the License at
11  *
12  *        http://www.apache.org/licenses/LICENSE-2.0
13  *
14  *  Unless required by applicable law or agreed to in writing, software
15  *  distributed under the License is distributed on an "AS IS" BASIS,
16  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  *  See the License for the specific language governing permissions and
18  *  limitations under the License.
19  *
20  *  SPDX-License-Identifier: Apache-2.0
21  *  ============LICENSE_END=========================================================
22  */
23
24 package org.onap.cps.spi.impl
25
26 import com.google.common.collect.ImmutableSet
27 import org.onap.cps.spi.CpsDataPersistenceService
28 import org.onap.cps.spi.entities.FragmentEntity
29 import org.onap.cps.spi.exceptions.AlreadyDefinedExceptionBatch
30 import org.onap.cps.spi.exceptions.AnchorNotFoundException
31 import org.onap.cps.spi.exceptions.CpsAdminException
32 import org.onap.cps.spi.exceptions.CpsPathException
33 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
34 import org.onap.cps.spi.exceptions.DataspaceNotFoundException
35 import org.onap.cps.spi.model.DataNode
36 import org.onap.cps.spi.model.DataNodeBuilder
37 import org.springframework.beans.factory.annotation.Autowired
38 import org.springframework.test.context.jdbc.Sql
39
40 import javax.validation.ConstraintViolationException
41
42 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
43 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
44
45 class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
46
47     @Autowired
48     CpsDataPersistenceService objectUnderTest
49
50     static DataNodeBuilder dataNodeBuilder = new DataNodeBuilder()
51
52     static final String SET_DATA = '/data/fragment.sql'
53     static long ID_DATA_NODE_WITH_DESCENDANTS = 4001
54     static String XPATH_DATA_NODE_WITH_DESCENDANTS = '/parent-1'
55     static long DATA_NODE_202_FRAGMENT_ID = 4202L
56     static long CHILD_OF_DATA_NODE_202_FRAGMENT_ID = 4203L
57     static long LIST_DATA_NODE_PARENT201_FRAGMENT_ID = 4206L
58     static long LIST_DATA_NODE_PARENT203_FRAGMENT_ID = 4214L
59     static long LIST_DATA_NODE_PARENT202_FRAGMENT_ID = 4211L
60     static long PARENT_3_FRAGMENT_ID = 4003L
61
62     static Collection<DataNode> newDataNodes = [new DataNodeBuilder().build()]
63     static Collection<DataNode> existingDataNodes = [createDataNodeTree(XPATH_DATA_NODE_WITH_DESCENDANTS)]
64     static Collection<DataNode> existingChildDataNodes = [createDataNodeTree('/parent-1/child-1')]
65
66     def static deleteTestParentXPath = '/parent-200'
67     def static deleteTestChildXpath = "${deleteTestParentXPath}/child-with-slash[@key='a/b']"
68     def static deleteTestGrandChildXPath = "${deleteTestChildXpath}/grandChild"
69
70     @Sql([CLEAR_DATA, SET_DATA])
71     def 'Get all datanodes with descendants .'() {
72         when: 'data nodes are retrieved by their xpath'
73             def dataNodes = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_NAME1, ['/parent-1'], INCLUDE_ALL_DESCENDANTS)
74         then: 'same data nodes are returned by getDataNodesForMultipleXpaths method'
75             assert objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME1, '/parent-1', INCLUDE_ALL_DESCENDANTS) == dataNodes
76         and: 'the dataNodes have no prefix (to be addressed by CPS-1301)'
77             assert dataNodes[0].moduleNamePrefix == null
78     }
79
80     @Sql([CLEAR_DATA, SET_DATA])
81     def 'Storing and Retrieving a new DataNodes with descendants.'() {
82         when: 'a fragment with descendants is stored'
83             def parentXpath = '/parent-new'
84             def childXpath = '/parent-new/child-new'
85             def grandChildXpath = '/parent-new/child-new/grandchild-new'
86             def dataNodes = [createDataNodeTree(parentXpath, childXpath, grandChildXpath)]
87             objectUnderTest.storeDataNodes(DATASPACE_NAME, ANCHOR_NAME1, dataNodes)
88         then: 'it can be retrieved by its xpath'
89             def dataNode = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, INCLUDE_ALL_DESCENDANTS)
90             assert dataNode[0].xpath == parentXpath
91         and: 'it has the correct child'
92             assert dataNode[0].childDataNodes.size() == 1
93             def childDataNode = dataNode[0].childDataNodes[0]
94             assert childDataNode.xpath == childXpath
95         and: 'and its grandchild'
96             assert childDataNode.childDataNodes.size() == 1
97             def grandChildDataNode = childDataNode.childDataNodes[0]
98             assert grandChildDataNode.xpath == grandChildXpath
99     }
100
101     @Sql([CLEAR_DATA, SET_DATA])
102     def 'Store data node for multiple anchors using the same schema.'() {
103         def xpath = '/parent-new'
104         given: 'a fragment is stored for an anchor'
105             objectUnderTest.storeDataNodes(DATASPACE_NAME, ANCHOR_NAME1, [createDataNodeTree(xpath)])
106         when: 'another fragment is stored for an other anchor, using the same schema set'
107             objectUnderTest.storeDataNodes(DATASPACE_NAME, ANCHOR_NAME3, [createDataNodeTree(xpath)])
108         then: 'both fragments can be retrieved by their xpath'
109             def fragment1 = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME1, xpath)
110             fragment1.anchor.name == ANCHOR_NAME1
111             fragment1.xpath == xpath
112             def fragment2 = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME3, xpath)
113             fragment2.anchor.name == ANCHOR_NAME3
114             fragment2.xpath == xpath
115     }
116
117     @Sql([CLEAR_DATA, SET_DATA])
118     def 'Store datanodes error scenario: #scenario.'() {
119         when: 'attempt to store a data node with #scenario'
120             objectUnderTest.storeDataNodes(dataspaceName, anchorName, dataNodes)
121         then: 'a #expectedException is thrown'
122             thrown(expectedException)
123         where: 'the following data is used'
124             scenario                    | dataspaceName  | anchorName     | dataNodes          || expectedException
125             'dataspace does not exist'  | 'unknown'      | 'not-relevant' | newDataNodes       || DataspaceNotFoundException
126             'schema set does not exist' | DATASPACE_NAME | 'unknown'      | newDataNodes       || AnchorNotFoundException
127             'anchor already exists'     | DATASPACE_NAME | ANCHOR_NAME1   | newDataNodes       || ConstraintViolationException
128             'datanode already exists'   | DATASPACE_NAME | ANCHOR_NAME1   | existingDataNodes  || AlreadyDefinedExceptionBatch
129     }
130
131     @Sql([CLEAR_DATA, SET_DATA])
132     def 'Add children to a Fragment that already has a child.'() {
133         given: 'collection of new child data nodes'
134             def newChild1 = createDataNodeTree('/parent-1/child-2')
135             def newChild2 = createDataNodeTree('/parent-1/child-3')
136             def newChildrenCollection = [newChild1, newChild2]
137         when: 'the child is added to an existing parent with 1 child'
138             objectUnderTest.addChildDataNodes(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, newChildrenCollection)
139         then: 'the parent is now has to 3 children'
140             def expectedExistingChildPath = '/parent-1/child-1'
141             def parentFragment = fragmentRepository.findById(ID_DATA_NODE_WITH_DESCENDANTS).orElseThrow()
142             parentFragment.childFragments.size() == 3
143         and: 'it still has the old child'
144             parentFragment.childFragments.find({ it.xpath == expectedExistingChildPath })
145         and: 'it has the new children'
146             parentFragment.childFragments.find({ it.xpath == newChildrenCollection[0].xpath })
147             parentFragment.childFragments.find({ it.xpath == newChildrenCollection[1].xpath })
148     }
149
150     @Sql([CLEAR_DATA, SET_DATA])
151     def 'Add child error scenario: #scenario.'() {
152         when: 'attempt to add a child data node with #scenario'
153             objectUnderTest.addChildDataNodes(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, dataNodes)
154         then: 'a #expectedException is thrown'
155             thrown(expectedException)
156         where: 'the following data is used'
157             scenario                 | parentXpath                      | dataNodes               || expectedException
158             'parent does not exist'  | '/unknown'                       | newDataNodes            || DataNodeNotFoundException
159             'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNodes  || AlreadyDefinedExceptionBatch
160     }
161
162     @Sql([CLEAR_DATA, SET_DATA])
163     def 'Add collection of multiple new list elements including an element with a child datanode.'() {
164         given: 'two new child list elements for an existing parent'
165             def listElementXpaths = ['/parent-201/child-204[@key="NEW1"]', '/parent-201/child-204[@key="NEW2"]']
166             def listElements = toDataNodes(listElementXpaths)
167         and: 'a (grand)child data node for one of the new list elements'
168             def grandChild = buildDataNode('/parent-201/child-204[@key="NEW1"]/grand-child-204[@key2="NEW1-CHILD"]', [leave:'value'], [])
169             listElements[0].childDataNodes = [grandChild]
170         when: 'the new data node (list elements) are added to an existing parent node'
171             objectUnderTest.addMultipleLists(DATASPACE_NAME, ANCHOR_NAME3, '/parent-201', [listElements])
172         then: 'new entries are successfully persisted, parent node now contains 5 children (2 new + 3 existing before)'
173             def parentFragment = fragmentRepository.getReferenceById(LIST_DATA_NODE_PARENT201_FRAGMENT_ID)
174             def allChildXpaths = parentFragment.childFragments.collect { it.xpath }
175             assert allChildXpaths.size() == 5
176             assert allChildXpaths.containsAll(listElementXpaths)
177         and: 'the (grand)child node of the new list entry is also present'
178             def dataspaceEntity = dataspaceRepository.getByName(DATASPACE_NAME)
179             def anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, ANCHOR_NAME3)
180             def grandChildFragmentEntity = fragmentRepository.findByAnchorAndXpath(anchorEntity, grandChild.xpath)
181             assert grandChildFragmentEntity.isPresent()
182     }
183
184     @Sql([CLEAR_DATA, SET_DATA])
185     def 'Add multiple list with a mix of existing and new elements'() {
186         given: 'two new child list elements for an existing parent'
187             def existingDataNode = dataNodeBuilder.withXpath('/parent-207/child-001').withLeaves(['id': '001']).build()
188             def newDataNode1 = dataNodeBuilder.withXpath('/parent-207/child-new1').withLeaves(['id': 'new1']).build()
189             def newDataNode2 = dataNodeBuilder.withXpath('/parent-200/child-new2').withLeaves(['id': 'new2']).build()
190             def dataNodeList1 = [existingDataNode, newDataNode1]
191             def dataNodeList2 = [newDataNode2]
192         when: 'duplicate data node is requested to be added'
193             objectUnderTest.addMultipleLists(DATASPACE_NAME, ANCHOR_HAVING_SINGLE_TOP_LEVEL_FRAGMENT, '/', [dataNodeList1, dataNodeList2])
194         then: 'already defined batch exception is thrown'
195             def thrown = thrown(AlreadyDefinedExceptionBatch)
196         and: 'it only contains the xpath(s) of the duplicated elements'
197             assert thrown.alreadyDefinedXpaths.size() == 1
198             assert thrown.alreadyDefinedXpaths.contains('/parent-207/child-001')
199         and: 'it does NOT contains the xpaths of the new element that were not combined with existing elements'
200             assert !thrown.alreadyDefinedXpaths.contains('/parent-207/child-new1')
201             assert !thrown.alreadyDefinedXpaths.contains('/parent-207/child-new1')
202         and: 'the new entity is inserted correctly'
203             def dataspaceEntity = dataspaceRepository.getByName(DATASPACE_NAME)
204             def anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, ANCHOR_HAVING_SINGLE_TOP_LEVEL_FRAGMENT)
205             fragmentRepository.findByAnchorAndXpath(anchorEntity, '/parent-200/child-new2').isPresent()
206     }
207
208     @Sql([CLEAR_DATA, SET_DATA])
209     def 'Add list element error scenario: #scenario.'() {
210         given: 'list element as a collection of data nodes'
211             def listElements = toDataNodes(listElementXpaths)
212         when: 'attempt to add list elements to parent node'
213             objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME3, parentNodeXpath, listElements)
214         then: 'a #expectedException is thrown'
215             thrown(expectedException)
216         where: 'following parameters were used'
217             scenario                        | parentNodeXpath | listElementXpaths                   || expectedException
218             'parent node does not exist'    | '/unknown'      | ['irrelevant']                      || DataNodeNotFoundException
219             'data fragment already exists'  | '/parent-201'   | ["/parent-201/child-204[@key='A']"] || AlreadyDefinedExceptionBatch
220     }
221
222     @Sql([CLEAR_DATA, SET_DATA])
223     def 'Get all data nodes by single xpath without descendants : #scenario'() {
224         when: 'data nodes are requested'
225             def result = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS,
226                 [inputXPath], OMIT_DESCENDANTS)
227         then: 'data nodes under root are returned'
228             assert result.childDataNodes.size() == 2
229         and: 'no descendants of parent nodes are returned'
230             result.each {assert it.childDataNodes.size() == 0}
231         and: 'same data nodes are returned when V2 of get Data Nodes API is executed'
232             assert objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS,
233                 inputXPath, OMIT_DESCENDANTS) == result
234         where: 'the following xpath is used'
235             scenario      | inputXPath
236             'root xpath'  | '/'
237             'empty xpath' | ''
238     }
239
240     @Sql([CLEAR_DATA, SET_DATA])
241     def 'Cps Path query with syntax error throws a CPS Path Exception.'() {
242         when: 'trying to execute a query with a syntax (parsing) error'
243             objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, 'invalid-cps-path/child' , OMIT_DESCENDANTS)
244         then: 'exception is thrown'
245             def exceptionThrown = thrown(CpsPathException)
246             assert exceptionThrown.getDetails() == "failed to parse at line 1 due to extraneous input 'invalid-cps-path' expecting '/'"
247     }
248
249     @Sql([CLEAR_DATA, SET_DATA])
250     def 'Get all data nodes by single xpath with all descendants : #scenario'() {
251         when: 'data nodes are requested with all descendants'
252             def result = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS,
253                 [inputXPath], INCLUDE_ALL_DESCENDANTS)
254             def mappedResult = multipleTreesToFlatMapByXpath(new HashMap<>(), result)
255         then: 'data nodes are returned with all the descendants populated'
256             assert mappedResult.size() == 8
257             assert result.childDataNodes.size() == 2
258             assert mappedResult.get('/parent-208/child-001').childDataNodes.size() == 0
259             assert mappedResult.get('/parent-208/child-002').childDataNodes.size() == 1
260             assert mappedResult.get('/parent-209/child-001').childDataNodes.size() == 0
261             assert mappedResult.get('/parent-209/child-002').childDataNodes.size() == 1
262         and: 'same data nodes are returned when V2 of Get Data Nodes API is executed'
263             assert objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS,
264                 inputXPath, INCLUDE_ALL_DESCENDANTS) == result
265         where: 'the following data is used'
266             scenario      | inputXPath
267             'root xpath'  | '/'
268             'empty xpath' | ''
269     }
270
271     @Sql([CLEAR_DATA, SET_DATA])
272     def 'Get data nodes error scenario : #scenario.'() {
273         when: 'attempt to get data nodes with #scenario'
274             objectUnderTest.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS)
275         then: 'an #expectedException is thrown'
276             thrown(expectedException)
277         where: 'the following data is used'
278             scenario             | dataspaceName  | anchorName                        | xpath           || expectedException
279             'non existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NO-XPATH'     || DataNodeNotFoundException
280             'invalid Xpath'      | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'INVALID XPATH' || CpsPathException
281     }
282
283     @Sql([CLEAR_DATA, SET_DATA])
284     def 'Get data nodes for multiple xpaths.'() {
285         when: 'fetch #scenario.'
286             def results = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_NAME3, inputXpaths, OMIT_DESCENDANTS)
287         then: 'the expected number of data nodes are returned'
288             assert results.size() == expectedResultSize
289         where: 'following parameters were used'
290             scenario                               | inputXpaths                                     || expectedResultSize
291             '0 nodes'                              | []                                              || 0
292             '1 node'                               | ["/parent-200"]                                 || 1
293             '2 unique nodes'                       | ["/parent-200", "/parent-201"]                  || 2
294             '3 unique nodes'                       | ["/parent-200", "/parent-201", "/parent-202"]   || 3
295             '1 unique node with duplicate xpath'   | ["/parent-200", "/parent-200"]                  || 1
296             '2 unique nodes with duplicate xpath'  | ["/parent-200", "/parent-202", "/parent-200"]   || 2
297             'list element with key (single quote)' | ["/parent-201/child-204[@key='A']"]             || 1
298             'list element with key (double quote)' | ['/parent-201/child-204[@key="A"]']             || 1
299             'whole list (not implemented)'         | ["/parent-201/child-204"]                       || 0
300             'non-existing xpath'                   | ["/NO-XPATH"]                                   || 0
301             'existing and non-existing xpaths'     | ["/parent-200", "/NO-XPATH", "/parent-201"]     || 2
302             'invalid xpath'                        | ["INVALID XPATH"]                               || 0
303             'valid and invalid xpaths'             | ["/parent-200", "INVALID XPATH", "/parent-201"] || 2
304             'root xpath'                           | ["/"]                                           || 7
305             'empty (root) xpath'                   | [""]                                            || 7
306             'root and top-level xpaths'            | ["/", "/parent-200", "/parent-201"]             || 7
307             'root and child xpaths'                | ["/", "/parent-200/child-201"]                  || 7
308     }
309
310     @Sql([CLEAR_DATA, SET_DATA])
311     def 'Get data nodes for collection of xpath error scenario : #scenario.'() {
312         when: 'attempt to get data nodes with #scenario'
313             objectUnderTest.getDataNodesForMultipleXpaths(dataspaceName, anchorName, ['/not-relevant'], OMIT_DESCENDANTS)
314         then: 'a #expectedException is thrown'
315             thrown(expectedException)
316         where: 'the following data is used'
317             scenario                 | dataspaceName  | anchorName     || expectedException
318             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' || DataspaceNotFoundException
319             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'    || AnchorNotFoundException
320     }
321
322     @Sql([CLEAR_DATA, SET_DATA])
323     def 'Update data nodes and descendants by removing descendants.'() {
324         given: 'data nodes with leaves updated, no children'
325             def submittedDataNodes = [buildDataNode('/parent-200/child-201', ['leaf-value': 'new'], [])]
326         when: 'update data nodes and descendants is performed'
327             objectUnderTest.updateDataNodesAndDescendants(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNodes)
328         then: 'leaves have been updated for selected data node'
329             def updatedFragment = fragmentRepository.getReferenceById(DATA_NODE_202_FRAGMENT_ID)
330             def updatedLeaves = getLeavesMap(updatedFragment)
331             assert updatedLeaves.size() == 1
332             assert updatedLeaves.'leaf-value' == 'new'
333         and: 'updated entry has no children'
334             updatedFragment.childFragments.isEmpty()
335         and: 'previously attached child entry is removed from database'
336             fragmentRepository.findById(CHILD_OF_DATA_NODE_202_FRAGMENT_ID).isEmpty()
337     }
338
339     @Sql([CLEAR_DATA, SET_DATA])
340     def 'Update data nodes and descendants with new descendants'() {
341         given: 'data nodes with leaves updated, having child with old content'
342             def submittedDataNodes = [buildDataNode('/parent-200/child-201', ['leaf-value': 'new'], [
343                   buildDataNode('/parent-200/child-201/grand-child', ['leaf-value': 'original'], [])
344             ])]
345         when: 'update is performed including descendants'
346             objectUnderTest.updateDataNodesAndDescendants(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNodes)
347         then: 'leaves have been updated for selected data node'
348             def updatedFragment = fragmentRepository.getReferenceById(DATA_NODE_202_FRAGMENT_ID)
349             def updatedLeaves = getLeavesMap(updatedFragment)
350             assert updatedLeaves.size() == 1
351             assert updatedLeaves.'leaf-value' == 'new'
352         and: 'existing child entry is not updated as content is same'
353             def childFragment = updatedFragment.childFragments.iterator().next()
354             childFragment.xpath == '/parent-200/child-201/grand-child'
355             def childLeaves = getLeavesMap(childFragment)
356             assert childLeaves.'leaf-value' == 'original'
357     }
358
359     @Sql([CLEAR_DATA, SET_DATA])
360     def 'Update data nodes and descendants with same descendants but changed leaf value.'() {
361         given: 'data nodes with leaves updated, having child with old content'
362             def submittedDataNodes = [buildDataNode('/parent-200/child-201', ['leaf-value': 'new'], [
363                     buildDataNode('/parent-200/child-201/grand-child', ['leaf-value': 'new'], [])
364             ])]
365         when: 'update is performed including descendants'
366             objectUnderTest.updateDataNodesAndDescendants(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNodes)
367         then: 'leaves have been updated for selected data node'
368             def updatedFragment = fragmentRepository.getReferenceById(DATA_NODE_202_FRAGMENT_ID)
369             def updatedLeaves = getLeavesMap(updatedFragment)
370             assert updatedLeaves.size() == 1
371             assert updatedLeaves.'leaf-value' == 'new'
372         and: 'existing child entry is updated with the new content'
373             def childFragment = updatedFragment.childFragments.iterator().next()
374             childFragment.xpath == '/parent-200/child-201/grand-child'
375             def childLeaves = getLeavesMap(childFragment)
376             assert childLeaves.'leaf-value' == 'new'
377     }
378
379     @Sql([CLEAR_DATA, SET_DATA])
380     def 'Update data nodes and descendants with different descendants xpath'() {
381         given: 'data nodes with leaves updated, having child with old content'
382             def submittedDataNodes = [buildDataNode('/parent-200/child-201', ['leaf-value': 'new'], [
383                     buildDataNode('/parent-200/child-201/grand-child-new', ['leaf-value': 'new'], [])
384             ])]
385         when: 'update is performed including descendants'
386             objectUnderTest.updateDataNodesAndDescendants(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNodes)
387         then: 'leaves have been updated for selected data node'
388             def updatedFragment = fragmentRepository.getReferenceById(DATA_NODE_202_FRAGMENT_ID)
389             def updatedLeaves = getLeavesMap(updatedFragment)
390             assert updatedLeaves.size() == 1
391             assert updatedLeaves.'leaf-value' == 'new'
392         and: 'previously attached child entry is removed from database'
393             fragmentRepository.findById(CHILD_OF_DATA_NODE_202_FRAGMENT_ID).isEmpty()
394         and: 'new child entry is persisted'
395             def childFragment = updatedFragment.childFragments.iterator().next()
396             childFragment.xpath == '/parent-200/child-201/grand-child-new'
397             def childLeaves = getLeavesMap(childFragment)
398             assert childLeaves.'leaf-value' == 'new'
399     }
400
401     @Sql([CLEAR_DATA, SET_DATA])
402     def 'Update data nodes and descendants error scenario: #scenario.'() {
403         given: 'data nodes collection'
404             def submittedDataNodes = [buildDataNode(xpath, ['leaf-name': 'leaf-value'], [])]
405         when: 'attempt to update data node for #scenario'
406             objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, submittedDataNodes)
407         then: 'a #expectedException is thrown'
408             thrown(expectedException)
409         where: 'the following data is used'
410             scenario                 | dataspaceName  | anchorName                        | xpath                 || expectedException
411             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | '/not relevant'       || DataspaceNotFoundException
412             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | '/not relevant'       || AnchorNotFoundException
413     }
414
415     @Sql([CLEAR_DATA, SET_DATA])
416     def 'Update existing list with #scenario.'() {
417         given: 'a parent having a list of data nodes containing: #originalKeys (ech list element has a child too)'
418             def parentXpath = '/parent-3'
419             if (originalKeys.size() > 0) {
420                 def originalListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'original value', originalKeys, true)
421                 objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, originalListEntriesAsDataNodes)
422             }
423         and: 'each original list element has one child'
424             def originalParentFragment = fragmentRepository.getReferenceById(PARENT_3_FRAGMENT_ID)
425             originalParentFragment.childFragments.each {assert it.childFragments.size() == 1 }
426         when: 'it is updated with #scenario'
427             def replacementListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'new value', replacementKeys, false)
428             objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, replacementListEntriesAsDataNodes)
429         then: 'the result list ONLY contains the expected replacement elements'
430             def parentFragment = fragmentRepository.getReferenceById(PARENT_3_FRAGMENT_ID)
431             def allChildXpaths = parentFragment.childFragments.collect { it.xpath }
432             def expectedListEntriesAfterUpdateAsXpaths = keysToXpaths(parentXpath, replacementKeys)
433             assert allChildXpaths.size() == replacementKeys.size()
434             assert allChildXpaths.containsAll(expectedListEntriesAfterUpdateAsXpaths)
435         and: 'all the list elements have the new values'
436             assert parentFragment.childFragments.stream().allMatch(childFragment -> childFragment.attributes.contains('new value'))
437         and: 'there are no more grandchildren as none of the replacement list entries had a child'
438             parentFragment.childFragments.each {assert it.childFragments.size() == 0 }
439         where: 'the following replacement lists are applied'
440             scenario                                            | originalKeys | replacementKeys
441             'one existing entry only'                           | []           | ['NEW']
442             'multiple new entries'                              | []           | ['NEW1', 'NEW2']
443             'one new entry only (existing entries are deleted)' | ['A', 'B']   | ['NEW1', 'NEW2']
444             'one existing on new entry'                         | ['A', 'B']   | ['A', 'NEW']
445             'one existing entry only'                           | ['A', 'B']   | ['A']
446     }
447
448     @Sql([CLEAR_DATA, SET_DATA])
449     def 'Replacing existing list element with attributes and (grand)child.'() {
450         given: 'a parent with list elements A and B with attribute and grandchild tagged as "org"'
451             def parentXpath = '/parent-3'
452             def originalListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'org', ['A','B'], true)
453             objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, originalListEntriesAsDataNodes)
454         when: 'A is replaced with an entry with attribute and grandchild tagged tagged as "new" (B is not in replacement list)'
455             def replacementListEntriesAsDataNodes = createChildListAllHavingAttributeValue(parentXpath, 'new', ['A'], true)
456             objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, replacementListEntriesAsDataNodes)
457         then: 'The updated fragment has a child-list with ONLY element "A"'
458             def parentFragment = fragmentRepository.getReferenceById(PARENT_3_FRAGMENT_ID)
459             parentFragment.childFragments.size() == 1
460             def childListElementA = parentFragment.childFragments[0]
461             childListElementA.xpath == "/parent-3/child-list[@key='A']"
462         and: 'element "A" has an attribute with the "new" (tag) value'
463             childListElementA.attributes == '{"attr1": "new"}'
464         and: 'element "A" has a only one (grand)child'
465             childListElementA.childFragments.size() == 1
466         and: 'the grandchild is the new grandchild (tag)'
467             def grandChild = childListElementA.childFragments[0]
468             grandChild.xpath == "/parent-3/child-list[@key='A']/new-grand-child"
469         and: 'the grandchild has an attribute with the "new" (tag) value'
470             grandChild.attributes == '{"attr1": "new"}'
471     }
472
473     @Sql([CLEAR_DATA, SET_DATA])
474     def 'Replace list element for a parent (parent-1) with existing one (non-list) child'() {
475         when: 'a list element is added under the parent'
476             def replacementListEntriesAsDataNodes = createChildListAllHavingAttributeValue(XPATH_DATA_NODE_WITH_DESCENDANTS, 'new', ['A','B'], false)
477             objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, replacementListEntriesAsDataNodes)
478         then: 'the parent will have 3 children after the replacement'
479             def parentFragment = fragmentRepository.getReferenceById(ID_DATA_NODE_WITH_DESCENDANTS)
480             parentFragment.childFragments.size() == 3
481             def xpaths = parentFragment.childFragments.collect {it.xpath}
482         and: 'one of the children is the original child fragment'
483             xpaths.contains('/parent-1/child-1')
484         and: 'it has the two new list elements'
485             xpaths.containsAll("/parent-1/child-list[@key='A']", "/parent-1/child-list[@key='B']")
486     }
487
488     @Sql([CLEAR_DATA, SET_DATA])
489     def 'Replace list content using unknown parent'() {
490         given: 'list element as a collection of data nodes'
491             def listElementCollection = toDataNodes(['irrelevant'])
492         when: 'attempt to replace list elements under unknown parent node'
493             objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, '/unknown', listElementCollection)
494         then: 'a datanode not found exception is thrown'
495             thrown(DataNodeNotFoundException)
496     }
497
498     @Sql([CLEAR_DATA, SET_DATA])
499     def 'Replace list content with empty collection is not supported'() {
500         when: 'attempt to replace list elements with empty collection'
501             objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, '/parent-203', [])
502         then: 'a CPS admin exception is thrown'
503             def thrown = thrown(CpsAdminException)
504             assert thrown.message == 'Invalid list replacement'
505     }
506
507     @Sql([CLEAR_DATA, SET_DATA])
508     def 'Delete list scenario: #scenario.'() {
509         when: 'deleting list is executed for: #scenario.'
510             objectUnderTest.deleteListDataNode(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths)
511         and: 'remaining children are fetched'
512             def parentFragment = fragmentRepository.getReferenceById(parentFragmentId)
513             def remainingChildXpaths = parentFragment.childFragments.collect { it.xpath }
514         then: 'only the expected children remain'
515             assert remainingChildXpaths.size() == expectedRemainingChildXpaths.size()
516             assert remainingChildXpaths.containsAll(expectedRemainingChildXpaths)
517         where: 'following parameters were used'
518             scenario                          | targetXpaths                                                 | parentFragmentId                     || expectedRemainingChildXpaths
519             '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']"]
520             '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']"]
521             'whole list'                      | '/parent-203/child-204'                                      | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203']
522             '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']"]
523     }
524
525     @Sql([CLEAR_DATA, SET_DATA])
526     def 'Delete multiple data nodes using scenario: #scenario.'() {
527         when: 'deleting nodes is executed for: #scenario.'
528             objectUnderTest.deleteDataNodes(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths)
529         and: 'remaining children are fetched'
530             def parentFragment = fragmentRepository.getReferenceById(LIST_DATA_NODE_PARENT203_FRAGMENT_ID)
531             def remainingChildXpaths = parentFragment.childFragments.collect { it.xpath }
532         then: 'only the expected children remain'
533             assert remainingChildXpaths.size() == expectedRemainingChildXpaths.size()
534             assert remainingChildXpaths.containsAll(expectedRemainingChildXpaths)
535         where: 'following parameters were used'
536             scenario                          | targetXpaths                                                           || expectedRemainingChildXpaths
537             'delete nothing'                  | []                                                                     || ["/parent-203/child-203", "/parent-203/child-204[@key='A']", "/parent-203/child-204[@key='B']"]
538             'datanode'                        | ['/parent-203/child-203']                                              || ["/parent-203/child-204[@key='A']", "/parent-203/child-204[@key='B']"]
539             '1 list element'                  | ['/parent-203/child-204[@key="A"]']                                    || ["/parent-203/child-203", "/parent-203/child-204[@key='B']"]
540             '2 list elements'                 | ['/parent-203/child-204[@key="A"]', '/parent-203/child-204[@key="B"]'] || ["/parent-203/child-203"]
541             'whole list'                      | ['/parent-203/child-204']                                              || ['/parent-203/child-203']
542             'list and element in same list'   | ['/parent-203/child-204', '/parent-203/child-204[@key="A"]']           || ['/parent-203/child-203']
543             'list element under list element' | ['/parent-203/child-204[@key="B"]/grand-child-204[@key2="Y"]']         || ["/parent-203/child-203", "/parent-203/child-204[@key='A']", "/parent-203/child-204[@key='B']"]
544             'invalid xpath'                   | ['INVALID XPATH', '/parent-203/child-204']                             || ['/parent-203/child-203']
545     }
546
547     @Sql([CLEAR_DATA, SET_DATA])
548     def 'Delete multiple data nodes error scenario: #scenario.'() {
549         when: 'deleting nodes is executed for: #scenario.'
550             objectUnderTest.deleteDataNodes(dataspaceName, anchorName, targetXpaths)
551         then: 'a #expectedException is thrown'
552             thrown(expectedException)
553         where: 'the following data is used'
554             scenario                 | dataspaceName  | anchorName     | targetXpaths            || expectedException
555             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | ['/not relevant']       || DataspaceNotFoundException
556             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'    | ['/not relevant']       || AnchorNotFoundException
557             'non-existing datanode'  | DATASPACE_NAME | ANCHOR_NAME3   | ['/NON-EXISTING-XPATH'] || DataNodeNotFoundException
558     }
559
560     @Sql([CLEAR_DATA, SET_DATA])
561     def 'Delete data nodes with "/"-token in list key value: #scenario. (CPS-1409)'() {
562         given: 'a data nodes with list-element child with "/" in index value (and grandchild)'
563             def grandChild = new DataNodeBuilder().withXpath(deleteTestGrandChildXPath).build()
564             def child = new DataNodeBuilder().withXpath(deleteTestChildXpath).withChildDataNodes([grandChild]).build()
565             objectUnderTest.addChildDataNodes(DATASPACE_NAME, ANCHOR_NAME3, deleteTestParentXPath, [child])
566         and: 'number of children before delete is stored'
567             def numberOfChildrenBeforeDelete = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, pathToParentOfDeletedNode, INCLUDE_ALL_DESCENDANTS)[0].childDataNodes.size()
568         when: 'target node is deleted'
569             objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, deleteTarget)
570         then: 'one child has been deleted'
571             def numberOfChildrenAfterDelete = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, pathToParentOfDeletedNode, INCLUDE_ALL_DESCENDANTS)[0].childDataNodes.size()
572             assert numberOfChildrenAfterDelete == numberOfChildrenBeforeDelete - 1
573         where:
574             scenario                | deleteTarget              | pathToParentOfDeletedNode
575             'list element with /'   | deleteTestChildXpath      | deleteTestParentXPath
576             'child of list element' | deleteTestGrandChildXPath | deleteTestChildXpath
577     }
578
579     @Sql([CLEAR_DATA, SET_DATA])
580     def 'Delete list error scenario: #scenario.'() {
581         when: 'attempting to delete scenario: #scenario.'
582             objectUnderTest.deleteListDataNode(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths)
583         then: 'a DataNodeNotFoundException is thrown'
584             thrown(DataNodeNotFoundException)
585         where: 'following parameters were used'
586             scenario                                   | targetXpaths
587             'whole list, parent node does not exist'   | '/unknown/some-child'
588             'list element, parent node does not exist' | '/unknown/child-204[@key="A"]'
589             'whole list does not exist'                | '/parent-200/unknown'
590             'list element, list does not exist'        | '/parent-200/unknown[@key="C"]'
591             'list element, element does not exist'     | '/parent-203/child-204[@key="C"]'
592             'valid datanode but not a list'            | '/parent-200/child-202'
593     }
594
595     @Sql([CLEAR_DATA, SET_DATA])
596     def 'Delete data node by xpath #scenario.'() {
597         given: 'a valid data node'
598             def dataNode
599         and: 'data nodes are deleted'
600             objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion)
601         when: 'verify data nodes are removed'
602             objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion, INCLUDE_ALL_DESCENDANTS)
603         then:
604             thrown(DataNodeNotFoundException)
605         and: 'some related object is not deleted'
606             if (xpathSurvivor!=null) {
607                 dataNode = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, xpathSurvivor, INCLUDE_ALL_DESCENDANTS)
608                 assert dataNode[0].xpath == xpathSurvivor
609             }
610         where: 'following parameters were used'
611             scenario                               | xpathForDeletion                                  || xpathSurvivor
612             'child data node, parent still exists' | '/parent-206/child-206'                           || '/parent-206'
613             'list element, sibling still exists'   | '/parent-206/child-206/grand-child-206[@key="A"]' || "/parent-206/child-206/grand-child-206[@key='X']"
614             'container node'                       | '/parent-206'                                     || null
615             'container list node'                  | '/parent-206[@key="A"]'                           || "/parent-206[@key='B']"
616             'root node with xpath /'               | '/'                                               || null
617             'root node with xpath passed as blank' | ''                                                || null
618     }
619
620     @Sql([CLEAR_DATA, SET_DATA])
621     def 'Delete data node error scenario: #scenario.'() {
622         when: 'data node is deleted'
623             objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, datanodeXpath)
624         then: 'a #expectedException is thrown'
625             thrown(expectedException)
626         where: 'the following parameters were used'
627             scenario                                   | datanodeXpath                                    | expectedException
628             'valid data node, non existent child node' | '/parent-203/child-non-existent'                 | DataNodeNotFoundException
629             'invalid list element'                     | '/parent-206/child-206/grand-child-206@key="A"]' | CpsPathException
630     }
631
632     @Sql([CLEAR_DATA, SET_DATA])
633     def 'Delete data node for an anchor.'() {
634         given: 'a data-node exists for an anchor'
635             assert fragmentsExistInDB(3003)
636         when: 'data nodes are deleted '
637             objectUnderTest.deleteDataNodes(DATASPACE_NAME, ANCHOR_NAME3)
638         then: 'all data-nodes are deleted successfully'
639             assert !fragmentsExistInDB(3003)
640     }
641
642     @Sql([CLEAR_DATA, SET_DATA])
643     def 'Delete data node for multiple anchors.'() {
644         given: 'a data-node exists for an anchor'
645             assert fragmentsExistInDB(3001)
646             assert fragmentsExistInDB(3003)
647         when: 'data nodes are deleted '
648             objectUnderTest.deleteDataNodes(DATASPACE_NAME, ['ANCHOR-001', 'ANCHOR-003'])
649         then: 'all data-nodes are deleted successfully'
650             assert !fragmentsExistInDB(3001)
651             assert !fragmentsExistInDB(3003)
652     }
653
654     def fragmentsExistInDB(anchorId) {
655         fragmentRepository.existsByAnchorId(anchorId)
656     }
657
658     static Collection<DataNode> toDataNodes(xpaths) {
659         return xpaths.collect { new DataNodeBuilder().withXpath(it).build() }
660     }
661
662
663     static DataNode buildDataNode(xpath, leaves, childDataNodes) {
664         return dataNodeBuilder.withXpath(xpath).withLeaves(leaves).withChildDataNodes(childDataNodes).build()
665     }
666
667     Map<String, Object> getLeavesMap(FragmentEntity fragmentEntity) {
668         return jsonObjectMapper.convertJsonString(fragmentEntity.attributes, Map<String, Object>.class)
669     }
670
671     def static treeToFlatMapByXpath(Map<String, DataNode> flatMap, DataNode dataNodeTree) {
672         flatMap.put(dataNodeTree.xpath, dataNodeTree)
673         dataNodeTree.childDataNodes
674                 .forEach(childDataNode -> treeToFlatMapByXpath(flatMap, childDataNode))
675         return flatMap
676     }
677
678     def static multipleTreesToFlatMapByXpath(Map<String, DataNode> flatMap, Collection<DataNode> dataNodeTrees) {
679         for (DataNode dataNodeTree: dataNodeTrees){
680             flatMap.put(dataNodeTree.xpath, dataNodeTree)
681             dataNodeTree.childDataNodes
682                 .forEach(childDataNode -> multipleTreesToFlatMapByXpath(flatMap, [childDataNode]))
683         }
684         return flatMap
685     }
686
687     def keysToXpaths(parent, Collection keys) {
688         return keys.collect { "${parent}/child-list[@key='${it}']".toString() }
689     }
690
691     def static createDataNodeTree(String... xpaths) {
692         def dataNodeBuilder = new DataNodeBuilder().withXpath(xpaths[0])
693         if (xpaths.length > 1) {
694             def xPathsDescendant = Arrays.copyOfRange(xpaths, 1, xpaths.length)
695             def childDataNode = createDataNodeTree(xPathsDescendant)
696             dataNodeBuilder.withChildDataNodes(ImmutableSet.of(childDataNode))
697         }
698         dataNodeBuilder.build()
699     }
700
701     def getFragmentByXpath(dataspaceName, anchorName, xpath) {
702         def dataspace = dataspaceRepository.getByName(dataspaceName)
703         def anchor = anchorRepository.getByDataspaceAndName(dataspace, anchorName)
704         return fragmentRepository.findByAnchorAndXpath(anchor, xpath).orElseThrow()
705     }
706
707     def createChildListAllHavingAttributeValue(parentXpath, tag, Collection keys, boolean addGrandChild) {
708         def listElementAsDataNodes = keysToXpaths(parentXpath, keys).collect {
709                 new DataNodeBuilder()
710                     .withXpath(it)
711                     .withLeaves([attr1: tag])
712                     .build()
713         }
714         if (addGrandChild) {
715             listElementAsDataNodes.each {it.childDataNodes = [createGrandChild(it.xpath, tag)]}
716         }
717         return listElementAsDataNodes
718     }
719
720     def createGrandChild(parentXPath, tag) {
721         new DataNodeBuilder()
722             .withXpath("${parentXPath}/${tag}-grand-child")
723             .withLeaves([attr1: tag])
724             .build()
725     }
726
727 }