Internal Server Error when creating the same data node twice
[cps.git] / cps-ri / src / test / groovy / org / onap / cps / spi / impl / CpsDataPersistenceServiceSpec.groovy
1 /*
2  * ============LICENSE_START=======================================================
3  *  Copyright (C) 2021 Nordix Foundation
4  *  Modifications Copyright (C) 2021 Pantheon.tech
5  *  ================================================================================
6  *  Licensed under the Apache License, Version 2.0 (the "License");
7  *  you may not use this file except in compliance with the License.
8  *  You may obtain a copy of the License at
9  *
10  *        http://www.apache.org/licenses/LICENSE-2.0
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an "AS IS" BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  *
17  *  SPDX-License-Identifier: Apache-2.0
18  *  ============LICENSE_END=========================================================
19  */
20 package org.onap.cps.spi.impl
21
22 import com.google.common.collect.ImmutableSet
23 import com.google.gson.Gson
24 import com.google.gson.GsonBuilder
25 import org.onap.cps.spi.CpsDataPersistenceService
26 import org.onap.cps.spi.FetchDescendantsOption
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.DataNodeNotFoundException
31 import org.onap.cps.spi.exceptions.DataspaceNotFoundException
32 import org.onap.cps.spi.model.DataNode
33 import org.onap.cps.spi.model.DataNodeBuilder
34 import org.springframework.beans.factory.annotation.Autowired
35 import org.springframework.dao.DataIntegrityViolationException
36 import org.springframework.test.context.jdbc.Sql
37 import spock.lang.Unroll
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 CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase {
45
46     @Autowired
47     CpsDataPersistenceService objectUnderTest
48
49     static final Gson GSON = new GsonBuilder().create()
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
58     static final DataNode newDataNode = new DataNodeBuilder().build()
59     static DataNode existingDataNode
60     static DataNode existingChildDataNode
61
62     def expectedLeavesByXpathMap = [
63             '/parent-100'                      : ['parent-leaf': 'parent-leaf-value'],
64             '/parent-100/child-001'            : ['first-child-leaf': 'first-child-leaf-value'],
65             '/parent-100/child-002'            : ['second-child-leaf': 'second-child-leaf-value'],
66             '/parent-100/child-002/grand-child': ['grand-child-leaf': 'grand-child-leaf-value']
67     ]
68
69     static {
70         existingDataNode = createDataNodeTree(XPATH_DATA_NODE_WITH_DESCENDANTS)
71         existingChildDataNode = createDataNodeTree('/parent-1/child-1')
72     }
73
74     @Sql([CLEAR_DATA, SET_DATA])
75     def 'StoreDataNode with descendants.'() {
76         when: 'a fragment with descendants is stored'
77             def parentXpath = "/parent-new"
78             def childXpath = "/parent-new/child-new"
79             def grandChildXpath = "/parent-new/child-new/grandchild-new"
80             objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1,
81                     createDataNodeTree(parentXpath, childXpath, grandChildXpath))
82         then: 'it can be retrieved by its xpath'
83             def parentFragment = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME1, parentXpath)
84         and: 'it contains the children'
85             parentFragment.childFragments.size() == 1
86             def childFragment = parentFragment.childFragments[0]
87             childFragment.xpath == childXpath
88         and: "and its children's children"
89             childFragment.childFragments.size() == 1
90             def grandchildFragment = childFragment.childFragments[0]
91             grandchildFragment.xpath == grandChildXpath
92     }
93
94     @Unroll
95     @Sql([CLEAR_DATA, SET_DATA])
96     def 'Store datanode error scenario: #scenario.'() {
97         when: 'attempt to store a data node with #scenario'
98             objectUnderTest.storeDataNode(dataspaceName, anchorName, dataNode)
99         then: 'a #expectedException is thrown'
100             thrown(expectedException)
101         where: 'the following data is used'
102             scenario                    | dataspaceName  | anchorName     | dataNode         || expectedException
103             'dataspace does not exist'  | 'unknown'      | 'not-relevant' | newDataNode      || DataspaceNotFoundException
104             'schema set does not exist' | DATASPACE_NAME | 'unknown'      | newDataNode      || AnchorNotFoundException
105             'anchor already exists'     | DATASPACE_NAME | ANCHOR_NAME1   | newDataNode      || ConstraintViolationException
106             'datanode already exists'   | DATASPACE_NAME | ANCHOR_NAME1   | existingDataNode || AlreadyDefinedException
107     }
108
109     @Sql([CLEAR_DATA, SET_DATA])
110     def 'Add a child to a Fragment that already has a child.'() {
111         given: ' a new child node'
112             def newChild = createDataNodeTree('xpath for new child')
113         when: 'the child is added to an existing parent with 1 child'
114             objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, newChild)
115         then: 'the parent is now has to 2 children'
116             def expectedExistingChildPath = '/parent-1/child-1'
117             def parentFragment = fragmentRepository.findById(ID_DATA_NODE_WITH_DESCENDANTS).orElseThrow()
118             parentFragment.getChildFragments().size() == 2
119         and: 'it still has the old child'
120             parentFragment.getChildFragments().find({ it.xpath == expectedExistingChildPath })
121         and: 'it has the new child'
122             parentFragment.getChildFragments().find({ it.xpath == newChild.xpath })
123     }
124
125     @Unroll
126     @Sql([CLEAR_DATA, SET_DATA])
127     def 'Add child error scenario: #scenario.'() {
128         when: 'attempt to add a child data node with #scenario'
129             objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, dataNode)
130         then: 'a #expectedException is thrown'
131             thrown(expectedException)
132         where: 'the following data is used'
133             scenario                 | parentXpath                      | dataNode              || expectedException
134             'parent does not exist'  | 'unknown'                        | newDataNode           || DataNodeNotFoundException
135             'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNode || DataIntegrityViolationException
136     }
137
138     static def createDataNodeTree(String... xpaths) {
139         def dataNodeBuilder = new DataNodeBuilder().withXpath(xpaths[0])
140         if (xpaths.length > 1) {
141             def xPathsDescendant = Arrays.copyOfRange(xpaths, 1, xpaths.length)
142             def childDataNode = createDataNodeTree(xPathsDescendant)
143             dataNodeBuilder.withChildDataNodes(ImmutableSet.of(childDataNode))
144         }
145         dataNodeBuilder.build()
146     }
147
148     def getFragmentByXpath(dataspaceName, anchorName, xpath) {
149         def dataspace = dataspaceRepository.getByName(dataspaceName)
150         def anchor = anchorRepository.getByDataspaceAndName(dataspace, anchorName)
151         return fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspace, anchor, xpath).orElseThrow()
152     }
153
154     @Sql([CLEAR_DATA, SET_DATA])
155     def 'Get data node by xpath without descendants.'() {
156         when: 'data node is requested'
157             def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
158                     XPATH_DATA_NODE_WITH_LEAVES, OMIT_DESCENDANTS)
159         then: 'data node is returned with no descendants'
160             assert result.getXpath() == XPATH_DATA_NODE_WITH_LEAVES
161         and: 'expected leaves'
162             assert result.getChildDataNodes().size() == 0
163             assertLeavesMaps(result.getLeaves(), expectedLeavesByXpathMap[XPATH_DATA_NODE_WITH_LEAVES])
164     }
165
166     @Sql([CLEAR_DATA, SET_DATA])
167     def 'Get data node by xpath with all descendants.'() {
168         when: 'data node is requested with all descendants'
169             def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
170                     XPATH_DATA_NODE_WITH_LEAVES, INCLUDE_ALL_DESCENDANTS)
171             def mappedResult = treeToFlatMapByXpath(new HashMap<>(), result)
172         then: 'data node is returned with all the descendants populated'
173             assert mappedResult.size() == 4
174             assert result.getChildDataNodes().size() == 2
175             assert mappedResult.get('/parent-100/child-001').getChildDataNodes().size() == 0
176             assert mappedResult.get('/parent-100/child-002').getChildDataNodes().size() == 1
177         and: 'extracted leaves maps are matching expected'
178             mappedResult.forEach(
179                     (xpath, dataNode) ->
180                             assertLeavesMaps(dataNode.getLeaves(), expectedLeavesByXpathMap[xpath])
181             )
182     }
183
184     def static assertLeavesMaps(actualLeavesMap, expectedLeavesMap) {
185         expectedLeavesMap.forEach((key, value) -> {
186             def actualValue = actualLeavesMap[key]
187             if (value instanceof Collection<?> && actualValue instanceof Collection<?>) {
188                 assert value.size() == actualValue.size()
189                 assert value.containsAll(actualValue)
190             } else {
191                 assert value == actualValue
192             }
193         }
194         )
195         return true
196     }
197
198     def static treeToFlatMapByXpath(Map<String, DataNode> flatMap, DataNode dataNodeTree) {
199         flatMap.put(dataNodeTree.getXpath(), dataNodeTree)
200         dataNodeTree.getChildDataNodes()
201                 .forEach(childDataNode -> treeToFlatMapByXpath(flatMap, childDataNode))
202         return flatMap
203     }
204
205     @Unroll
206     @Sql([CLEAR_DATA, SET_DATA])
207     def 'Get data node error scenario: #scenario.'() {
208         when: 'attempt to get data node with #scenario'
209             objectUnderTest.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS)
210         then: 'a #expectedException is thrown'
211             thrown(expectedException)
212         where: 'the following data is used'
213             scenario                 | dataspaceName  | anchorName                        | xpath          || expectedException
214             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | 'not relevant' || DataspaceNotFoundException
215             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | 'not relevant' || AnchorNotFoundException
216             'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NO XPATH'     || DataNodeNotFoundException
217     }
218
219     @Sql([CLEAR_DATA, SET_DATA])
220     def 'Update data node leaves.'() {
221         when: 'update is performed for leaves'
222             objectUnderTest.updateDataLeaves(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
223                     "/parent-200/child-201", ['leaf-value': 'new'])
224         then: 'leaves are updated for selected data node'
225             def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
226             def updatedLeaves = getLeavesMap(updatedFragment)
227             assert updatedLeaves.size() == 1
228             assert updatedLeaves.'leaf-value' == 'new'
229         and: 'existing child entry remains as is'
230             def childFragment = updatedFragment.getChildFragments().iterator().next()
231             def childLeaves = getLeavesMap(childFragment)
232             assert childFragment.getId() == UPDATE_DATA_NODE_SUB_FRAGMENT_ID
233             assert childLeaves.'leaf-value' == 'original'
234     }
235
236     @Unroll
237     @Sql([CLEAR_DATA, SET_DATA])
238     def 'Update data leaves error scenario: #scenario.'() {
239         when: 'attempt to update data node for #scenario'
240             objectUnderTest.updateDataLeaves(dataspaceName, anchorName, xpath, ['leaf-name': 'leaf-value'])
241         then: 'a #expectedException is thrown'
242             thrown(expectedException)
243         where: 'the following data is used'
244             scenario                 | dataspaceName  | anchorName                        | xpath                || expectedException
245             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | 'not relevant'       || DataspaceNotFoundException
246             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | 'not relevant'       || AnchorNotFoundException
247             'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
248     }
249
250     @Sql([CLEAR_DATA, SET_DATA])
251     def 'Replace data node tree with descendants removal.'() {
252         given: 'data node object with leaves updated, no children'
253             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [])
254         when: 'replace data node tree is performed'
255             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
256         then: 'leaves have been updated for selected data node'
257             def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
258             def updatedLeaves = getLeavesMap(updatedFragment)
259             assert updatedLeaves.size() == 1
260             assert updatedLeaves.'leaf-value' == 'new'
261         and: 'updated entry has no children'
262             updatedFragment.getChildFragments().isEmpty()
263         and: 'previously attached child entry is removed from database'
264             fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty()
265     }
266
267     @Sql([CLEAR_DATA, SET_DATA])
268     def 'Replace data node tree with descendants.'() {
269         given: 'data node object with leaves updated, having child with old content'
270             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
271                     buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'original'], [])
272             ])
273         when: 'update is performed including descendants'
274             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
275         then: 'leaves have been updated for selected data node'
276             def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
277             def updatedLeaves = getLeavesMap(updatedFragment)
278             assert updatedLeaves.size() == 1
279             assert updatedLeaves.'leaf-value' == 'new'
280         and: 'previously attached child entry is removed from database'
281             fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty()
282         and: 'new child entry with same content is created'
283             def childFragment = updatedFragment.getChildFragments().iterator().next()
284             def childLeaves = getLeavesMap(childFragment)
285             assert childFragment.getId() != UPDATE_DATA_NODE_SUB_FRAGMENT_ID
286             assert childLeaves.'leaf-value' == 'original'
287     }
288
289     @Unroll
290     @Sql([CLEAR_DATA, SET_DATA])
291     def 'Replace data node tree error scenario: #scenario.'() {
292         given: 'data node object'
293             def submittedDataNode = buildDataNode(xpath, ['leaf-name': 'leaf-value'], [])
294         when: 'attempt to update data node for #scenario'
295             objectUnderTest.replaceDataNodeTree(dataspaceName, anchorName, submittedDataNode)
296         then: 'a #expectedException is thrown'
297             thrown(expectedException)
298         where: 'the following data is used'
299             scenario                 | dataspaceName  | anchorName                        | xpath                || expectedException
300             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | 'not relevant'       || DataspaceNotFoundException
301             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | 'not relevant'       || AnchorNotFoundException
302             'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
303     }
304
305     static DataNode buildDataNode(xpath, leaves, childDataNodes) {
306         return new DataNodeBuilder().withXpath(xpath).withLeaves(leaves).withChildDataNodes(childDataNodes).build()
307     }
308
309     static Map<String, Object> getLeavesMap(FragmentEntity fragmentEntity) {
310         return GSON.fromJson(fragmentEntity.getAttributes(), Map<String, Object>.class)
311     }
312
313     @Unroll
314     @Sql([CLEAR_DATA, SET_DATA])
315     def 'Cps Path query for single leaf value with type: #type.'() {
316         when: 'a query is executed to get a data node by the given cps path'
317             def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, cpsPath, includeDescendantsOption)
318         then: 'the correct data is returned'
319             def leaves = '[common-leaf-name:common-leaf-value, common-leaf-name-int:5.0]'
320             DataNode dataNode = result.stream().findFirst().get()
321             dataNode.getLeaves().toString() == leaves
322             dataNode.getChildDataNodes().size() == expectedNumberOfChidlNodes
323         where: 'the following data is used'
324             type                        | cpsPath                                                          | includeDescendantsOption | expectedNumberOfChidlNodes
325             'String and no descendants' | '/parent-200/child-202[@common-leaf-name=\'common-leaf-value\']' | OMIT_DESCENDANTS         | 0
326             'Integer and descendants'   | '/parent-200/child-202[@common-leaf-name-int=5]'                 | INCLUDE_ALL_DESCENDANTS  | 1
327     }
328
329     @Unroll
330     @Sql([CLEAR_DATA, SET_DATA])
331     def 'Query for attribute by cps path with cps paths that return no data because of #scenario.'() {
332         when: 'a query is executed to get datanodes for the given cps path'
333             def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, cpsPath, FetchDescendantsOption.OMIT_DESCENDANTS)
334         then: 'no data is returned'
335             result.isEmpty()
336         where: 'following cps queries are performed'
337             scenario                           | cpsPath
338             'cps path is incomplete'           | '/parent-200[@common-leaf-name-int=5]'
339             'leaf value does not exist'        | '/parent-200/child-202[@common-leaf-name=\'does not exist\']'
340             'incomplete end of xpath prefix'   | '/parent-200/child-20[@common-leaf-name-int=5]'
341             'empty cps path of type ends with' | '///'
342     }
343
344     @Unroll
345     @Sql([CLEAR_DATA, SET_DATA])
346     def 'Cps Path query with and without descendants using #type.'() {
347         when: 'a query is executed to get a data node by the given cps path'
348             def cpsPath = '///child-202'
349             def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, cpsPath, includeDescendantsOption)
350         then: 'the data node has the correct number of children'
351             DataNode dataNode = result.stream().findFirst().get()
352             dataNode.getChildDataNodes().size() == expectedNumberOfChildNodes
353         where: 'the following data is used'
354             type                                    | includeDescendantsOption | expectedNumberOfChildNodes
355             'ends with and omit descendants'        | OMIT_DESCENDANTS         | 0
356             'ends with and include all descendants' | INCLUDE_ALL_DESCENDANTS  | 1
357     }
358
359     @Unroll
360     @Sql([CLEAR_DATA, SET_DATA])
361     def 'Cps Path query using ends with variations of #type.'() {
362         when: 'a query is executed to get a data node by the given cps path'
363             def result = objectUnderTest.queryDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, cpsPath, OMIT_DESCENDANTS)
364         then: 'the correct number of data nodes is returned'
365             result.size() == expectedNumberOfDataNodes
366         where: 'the following data is used'
367             type                            | cpsPath             | expectedNumberOfDataNodes
368             'single match with / prefix'    | '///child-202'      | 1
369             'single match without / prefix' | '//grand-child-202' | 1
370             'multiple matches'              | '//202'             | 2
371     }
372 }