Data fragment update by xpath #2 - persistence layer
[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 static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
23 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
24
25 import com.google.common.collect.ImmutableSet
26 import com.google.gson.Gson
27 import com.google.gson.GsonBuilder
28 import org.onap.cps.spi.CpsDataPersistenceService
29 import org.onap.cps.spi.entities.FragmentEntity
30 import org.onap.cps.spi.exceptions.AnchorNotFoundException
31 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
32 import org.onap.cps.spi.exceptions.DataspaceNotFoundException
33 import org.onap.cps.spi.model.DataNode
34 import org.onap.cps.spi.model.DataNodeBuilder
35 import org.springframework.beans.factory.annotation.Autowired
36 import org.springframework.dao.DataIntegrityViolationException
37 import org.springframework.test.context.jdbc.Sql
38 import spock.lang.Unroll
39
40 class CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase {
41
42     @Autowired
43     CpsDataPersistenceService objectUnderTest
44
45     static final Gson GSON = new GsonBuilder().create()
46
47     static final String SET_DATA = '/data/fragment.sql'
48     static final long ID_DATA_NODE_WITH_DESCENDANTS = 4001
49     static final String XPATH_DATA_NODE_WITH_DESCENDANTS = '/parent-1'
50     static final String XPATH_DATA_NODE_WITH_LEAVES = '/parent-100'
51     static final long UPDATE_DATA_NODE_FRAGMENT_ID = 4202L
52     static final long UPDATE_DATA_NODE_SUB_FRAGMENT_ID = 4203L
53
54     static final DataNode newDataNode = new DataNodeBuilder().build()
55     static DataNode existingDataNode
56     static DataNode existingChildDataNode
57
58     def expectedLeavesByXpathMap = [
59             '/parent-100'                      : ['parent-leaf': 'parent-leaf-value'],
60             '/parent-100/child-001'            : ['first-child-leaf': 'first-child-leaf-value'],
61             '/parent-100/child-002'            : ['second-child-leaf': 'second-child-leaf-value'],
62             '/parent-100/child-002/grand-child': ['grand-child-leaf': 'grand-child-leaf-value']
63     ]
64
65     static {
66         existingDataNode = createDataNodeTree(XPATH_DATA_NODE_WITH_DESCENDANTS)
67         existingChildDataNode = createDataNodeTree('/parent-1/child-1')
68     }
69
70     @Sql([CLEAR_DATA, SET_DATA])
71     def 'StoreDataNode with descendants.'() {
72         when: 'a fragment with descendants is stored'
73             def parentXpath = "/parent-new"
74             def childXpath = "/parent-new/child-new"
75             def grandChildXpath = "/parent-new/child-new/grandchild-new"
76             objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1,
77                     createDataNodeTree(parentXpath, childXpath, grandChildXpath))
78         then: 'it can be retrieved by its xpath'
79             def parentFragment = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME1, parentXpath)
80         and: 'it contains the children'
81             parentFragment.childFragments.size() == 1
82             def childFragment = parentFragment.childFragments[0]
83             childFragment.xpath == childXpath
84         and: "and its children's children"
85             childFragment.childFragments.size() == 1
86             def grandchildFragment = childFragment.childFragments[0]
87             grandchildFragment.xpath == grandChildXpath
88     }
89
90     @Sql([CLEAR_DATA, SET_DATA])
91     def 'Store datanode error scenario: #scenario.'() {
92         when: 'attempt to store a data node with #scenario'
93             objectUnderTest.storeDataNode(dataspaceName, anchorName, dataNode)
94         then: 'a #expectedException is thrown'
95             thrown(expectedException)
96         where: 'the following data is used'
97             scenario                    | dataspaceName  | anchorName     | dataNode         || expectedException
98             'dataspace does not exist'  | 'unknown'      | 'not-relevant' | newDataNode      || DataspaceNotFoundException
99             'schema set does not exist' | DATASPACE_NAME | 'unknown'      | newDataNode      || AnchorNotFoundException
100             'anchor already exists'     | DATASPACE_NAME | ANCHOR_NAME1   | existingDataNode || DataIntegrityViolationException
101     }
102
103     @Sql([CLEAR_DATA, SET_DATA])
104     def 'Add a child to a Fragment that already has a child.'() {
105         given: ' a new child node'
106             def newChild = createDataNodeTree('xpath for new child')
107         when: 'the child is added to an existing parent with 1 child'
108             objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, newChild)
109         then: 'the parent is now has to 2 children'
110             def expectedExistingChildPath = '/parent-1/child-1'
111             def parentFragment = fragmentRepository.findById(ID_DATA_NODE_WITH_DESCENDANTS).orElseThrow()
112             parentFragment.getChildFragments().size() == 2
113         and: 'it still has the old child'
114             parentFragment.getChildFragments().find({ it.xpath == expectedExistingChildPath })
115         and: 'it has the new child'
116             parentFragment.getChildFragments().find({ it.xpath == newChild.xpath })
117     }
118
119     @Sql([CLEAR_DATA, SET_DATA])
120     def 'Add child error scenario: #scenario.'() {
121         when: 'attempt to add a child data node with #scenario'
122             objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, dataNode)
123         then: 'a #expectedException is thrown'
124             thrown(expectedException)
125         where: 'the following data is used'
126             scenario                 | parentXpath                      | dataNode              || expectedException
127             'parent does not exist'  | 'unknown'                        | newDataNode           || DataNodeNotFoundException
128             'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNode || DataIntegrityViolationException
129     }
130
131     static def createDataNodeTree(String... xpaths) {
132         def dataNodeBuilder = new DataNodeBuilder().withXpath(xpaths[0])
133         if (xpaths.length > 1) {
134             def xPathsDescendant = Arrays.copyOfRange(xpaths, 1, xpaths.length)
135             def childDataNode = createDataNodeTree(xPathsDescendant)
136             dataNodeBuilder.withChildDataNodes(ImmutableSet.of(childDataNode))
137         }
138         dataNodeBuilder.build()
139     }
140
141     def getFragmentByXpath(dataspaceName, anchorName, xpath) {
142         def dataspace = dataspaceRepository.getByName(dataspaceName)
143         def anchor = anchorRepository.getByDataspaceAndName(dataspace, anchorName)
144         return fragmentRepository.findByDataspaceAndAnchorAndXpath(dataspace, anchor, xpath).orElseThrow()
145     }
146
147     @Sql([CLEAR_DATA, SET_DATA])
148     def 'Get data node by xpath without descendants.'() {
149         when: 'data node is requested'
150             def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
151                     XPATH_DATA_NODE_WITH_LEAVES, OMIT_DESCENDANTS)
152         then: 'data node is returned with no descendants'
153             assert result.getXpath() == XPATH_DATA_NODE_WITH_LEAVES
154         and: 'expected leaves'
155             assert result.getChildDataNodes().size() == 0
156             assertLeavesMaps(result.getLeaves(), expectedLeavesByXpathMap[XPATH_DATA_NODE_WITH_LEAVES])
157     }
158
159     @Sql([CLEAR_DATA, SET_DATA])
160     def 'Get data node by xpath with all descendants.'() {
161         when: 'data node is requested with all descendants'
162             def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
163                     XPATH_DATA_NODE_WITH_LEAVES, INCLUDE_ALL_DESCENDANTS)
164             def mappedResult = treeToFlatMapByXpath(new HashMap<>(), result)
165         then: 'data node is returned with all the descendants populated'
166             assert mappedResult.size() == 4
167             assert result.getChildDataNodes().size() == 2
168             assert mappedResult.get('/parent-100/child-001').getChildDataNodes().size() == 0
169             assert mappedResult.get('/parent-100/child-002').getChildDataNodes().size() == 1
170         and: 'extracted leaves maps are matching expected'
171             mappedResult.forEach(
172                     (xpath, dataNode) ->
173                             assertLeavesMaps(dataNode.getLeaves(), expectedLeavesByXpathMap[xpath])
174             )
175     }
176
177     def static assertLeavesMaps(actualLeavesMap, expectedLeavesMap) {
178         expectedLeavesMap.forEach((key, value) -> {
179             def actualValue = actualLeavesMap[key]
180             if (value instanceof Collection<?> && actualValue instanceof Collection<?>) {
181                 assert value.size() == actualValue.size()
182                 assert value.containsAll(actualValue)
183             } else {
184                 assert value == actualValue
185             }
186         }
187         )
188         return true
189     }
190
191     def static treeToFlatMapByXpath(Map<String, DataNode> flatMap, DataNode dataNodeTree) {
192         flatMap.put(dataNodeTree.getXpath(), dataNodeTree)
193         dataNodeTree.getChildDataNodes()
194                 .forEach(childDataNode -> treeToFlatMapByXpath(flatMap, childDataNode))
195         return flatMap
196     }
197
198     @Unroll
199     @Sql([CLEAR_DATA, SET_DATA])
200     def 'Get data node error scenario: #scenario.'() {
201         when: 'attempt to get data node with #scenario'
202             objectUnderTest.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS)
203         then: 'a #expectedException is thrown'
204             thrown(expectedException)
205         where: 'the following data is used'
206             scenario                 | dataspaceName  | anchorName                        | xpath          || expectedException
207             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | 'not relevant' || DataspaceNotFoundException
208             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | 'not relevant' || AnchorNotFoundException
209             'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NO XPATH'     || DataNodeNotFoundException
210     }
211
212     @Sql([CLEAR_DATA, SET_DATA])
213     def 'Update data node leaves.'() {
214         when: 'update is performed for leaves'
215             objectUnderTest.updateDataLeaves(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
216                     "/parent-200/child-201", ['leaf-value': 'new'])
217         then: 'leaves are updated for selected data node'
218             def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
219             def updatedLeaves = getLeavesMap(updatedFragment)
220             assert updatedLeaves.size() == 1
221             assert updatedLeaves.'leaf-value' == 'new'
222         and: 'existing child entry remains as is'
223             def childFragment = updatedFragment.getChildFragments().iterator().next()
224             def childLeaves = getLeavesMap(childFragment)
225             assert childFragment.getId() == UPDATE_DATA_NODE_SUB_FRAGMENT_ID
226             assert childLeaves.'leaf-value' == 'original'
227     }
228
229     @Unroll
230     @Sql([CLEAR_DATA, SET_DATA])
231     def 'Update data leaves error scenario: #scenario.'() {
232         when: 'attempt to update data node for #scenario'
233             objectUnderTest.updateDataLeaves(dataspaceName, anchorName, xpath, ['leaf-name': 'leaf-value'])
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 | 'NON-EXISTING XPATH' || DataNodeNotFoundException
241     }
242
243     @Sql([CLEAR_DATA, SET_DATA])
244     def 'Replace data node tree with descendants removal.'() {
245         given: 'data node object with leaves updated, no children'
246             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [])
247         when: 'replace data node tree is performed'
248             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
249         then: 'leaves have been updated for selected data node'
250             def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
251             def updatedLeaves = getLeavesMap(updatedFragment)
252             assert updatedLeaves.size() == 1
253             assert updatedLeaves.'leaf-value' == 'new'
254         and: 'updated entry has no children'
255             updatedFragment.getChildFragments().isEmpty()
256         and: 'previously attached child entry is removed from database'
257             fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty()
258     }
259
260     @Sql([CLEAR_DATA, SET_DATA])
261     def 'Replace data node tree with descendants.'() {
262         given: 'data node object with leaves updated, having child with old content'
263             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
264                     buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'original'], [])
265             ])
266         when: 'update is performed including descendants'
267             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
268         then: 'leaves have been updated for selected data node'
269             def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
270             def updatedLeaves = getLeavesMap(updatedFragment)
271             assert updatedLeaves.size() == 1
272             assert updatedLeaves.'leaf-value' == 'new'
273         and: 'previously attached child entry is removed from database'
274             fragmentRepository.findById(UPDATE_DATA_NODE_SUB_FRAGMENT_ID).isEmpty()
275         and: 'new child entry with same content is created'
276             def childFragment = updatedFragment.getChildFragments().iterator().next()
277             def childLeaves = getLeavesMap(childFragment)
278             assert childFragment.getId() != UPDATE_DATA_NODE_SUB_FRAGMENT_ID
279             assert childLeaves.'leaf-value' == 'original'
280     }
281
282     @Unroll
283     @Sql([CLEAR_DATA, SET_DATA])
284     def 'Replace data node tree error scenario: #scenario.'() {
285         given: 'data node object'
286             def submittedDataNode = buildDataNode(xpath, ['leaf-name':'leaf-value'], [])
287         when: 'attempt to update data node for #scenario'
288             objectUnderTest.replaceDataNodeTree(dataspaceName, anchorName, submittedDataNode)
289         then: 'a #expectedException is thrown'
290             thrown(expectedException)
291         where: 'the following data is used'
292             scenario                 | dataspaceName  | anchorName                        | xpath                || expectedException
293             'non-existing dataspace' | 'NO DATASPACE' | 'not relevant'                    | 'not relevant'       || DataspaceNotFoundException
294             'non-existing anchor'    | DATASPACE_NAME | 'NO ANCHOR'                       | 'not relevant'       || AnchorNotFoundException
295             'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
296     }
297
298     static DataNode buildDataNode(xpath, leaves, childDataNodes) {
299         return new DataNodeBuilder().withXpath(xpath).withLeaves(leaves).withChildDataNodes(childDataNodes).build()
300     }
301
302     static Map<String, Object> getLeavesMap(FragmentEntity fragmentEntity) {
303         return GSON.fromJson(fragmentEntity.getAttributes(), Map<String, Object>.class)
304     }
305 }