Merge "CPS-341 Support for multiple data tree instances under 1 anchor"
[cps.git] / cps-service / src / test / groovy / org / onap / cps / api / impl / CpsDataServiceImplSpec.groovy
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2021-2022 Nordix Foundation
4  *  Modifications Copyright (C) 2021 Pantheon.tech
5  *  Modifications Copyright (C) 2021-2022 Bell Canada.
6  *  Modifications Copyright (C) 2022 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.api.impl
25
26 import org.onap.cps.TestUtils
27 import org.onap.cps.api.CpsAdminService
28 import org.onap.cps.notification.NotificationService
29 import org.onap.cps.notification.Operation
30 import org.onap.cps.spi.CpsDataPersistenceService
31 import org.onap.cps.spi.FetchDescendantsOption
32 import org.onap.cps.spi.exceptions.DataValidationException
33 import org.onap.cps.spi.model.Anchor
34 import org.onap.cps.spi.model.DataNode
35 import org.onap.cps.spi.model.DataNodeBuilder
36 import org.onap.cps.yang.YangTextSchemaSourceSet
37 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
38 import spock.lang.Specification
39 import org.onap.cps.spi.utils.CpsValidator
40
41 import java.time.OffsetDateTime
42 import java.util.stream.Collectors
43
44 class CpsDataServiceImplSpec extends Specification {
45     def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService)
46     def mockCpsAdminService = Mock(CpsAdminService)
47     def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)
48     def mockNotificationService = Mock(NotificationService)
49     def mockCpsValidator = Mock(CpsValidator)
50
51     def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAdminService,
52             mockYangTextSchemaSourceSetCache, mockNotificationService, mockCpsValidator)
53
54     def setup() {
55         mockCpsAdminService.getAnchor(dataspaceName, anchorName) >> anchor
56     }
57
58     def dataspaceName = 'some-dataspace'
59     def anchorName = 'some-anchor'
60     def schemaSetName = 'some-schema-set'
61     def anchor = Anchor.builder().name(anchorName).schemaSetName(schemaSetName).build()
62     def observedTimestamp = OffsetDateTime.now()
63
64     def 'Saving json data.'() {
65         given: 'schema set for given anchor and dataspace references test-tree model'
66             setupSchemaSetMocks('multipleDataTree.yang')
67         when: 'save data method is invoked with test-tree json data'
68             def jsonData = TestUtils.getResourceFileContent('multiple-object-data.json')
69             objectUnderTest.saveData(dataspaceName, anchorName, jsonData, observedTimestamp)
70         then: 'the persistence service method is invoked with correct parameters'
71             1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
72                 { dataNode -> dataNode.xpath[index] == xpath })
73         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
74             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
75         and: 'data updated event is sent to notification service'
76             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/', Operation.CREATE, observedTimestamp)
77         where:
78             index   |   xpath
79                 0   | '/first-container'
80                 1   | '/last-container'
81
82     }
83
84     def 'Saving child data fragment under existing node.'() {
85         given: 'schema set for given anchor and dataspace references test-tree model'
86             setupSchemaSetMocks('test-tree.yang')
87         when: 'save data method is invoked with test-tree json data'
88             def jsonData = '{"branch": [{"name": "New"}]}'
89             objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
90         then: 'the persistence service method is invoked with correct parameters'
91             1 * mockCpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, '/test-tree',
92                 { dataNode -> dataNode.xpath[0] == '/test-tree/branch[@name=\'New\']' })
93         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
94             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
95         and: 'data updated event is sent to notification service'
96             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/test-tree', Operation.CREATE, observedTimestamp)
97     }
98
99     def 'Saving list element data fragment under existing node.'() {
100         given: 'schema set for given anchor and dataspace references test-tree model'
101             setupSchemaSetMocks('test-tree.yang')
102         when: 'save data method is invoked with list element json data'
103             def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
104             objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
105         then: 'the persistence service method is invoked with correct parameters'
106             1 * mockCpsDataPersistenceService.addListElements(dataspaceName, anchorName, '/test-tree',
107                 { dataNodeCollection ->
108                     {
109                         assert dataNodeCollection.size() == 2
110                         assert dataNodeCollection.collect { it.getXpath() }
111                             .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
112                     }
113                 }
114             )
115         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
116             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
117         and: 'data updated event is sent to notification service'
118             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/test-tree', Operation.UPDATE, observedTimestamp)
119     }
120
121     def 'Saving collection of a batch with data fragment under existing node.'() {
122         given: 'schema set for given anchor and dataspace references test-tree model'
123             setupSchemaSetMocks('test-tree.yang')
124         when: 'save data method is invoked with list element json data'
125             def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
126             objectUnderTest.saveListElementsBatch(dataspaceName, anchorName, '/test-tree', [jsonData], observedTimestamp)
127         then: 'the persistence service method is invoked with correct parameters'
128             1 * mockCpsDataPersistenceService.addMultipleLists(dataspaceName, anchorName, '/test-tree',_) >> {
129                 args -> {
130                     def listElementsCollection = args[3] as Collection<Collection<DataNode>>
131                     assert listElementsCollection.size() == 1
132                     def listOfXpaths = listElementsCollection.stream().flatMap(x -> x.stream()).map(it-> it.xpath).collect(Collectors.toList())
133                     assert listOfXpaths.size() == 2
134                     assert listOfXpaths.containsAll(['/test-tree/branch[@name=\'B\']','/test-tree/branch[@name=\'A\']'])
135                 }
136             }
137         and: 'data updated event is sent to notification service'
138             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/test-tree', Operation.UPDATE, observedTimestamp)
139     }
140
141     def 'Saving empty list element data fragment.'() {
142         given: 'schema set for given anchor and dataspace references test-tree model'
143             setupSchemaSetMocks('test-tree.yang')
144         when: 'save data method is invoked with an empty list'
145             def jsonData = '{"branch": []}'
146             objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
147         then: 'invalid data exception is thrown'
148             thrown(DataValidationException)
149     }
150
151     def 'Get data node with option #fetchDescendantsOption.'() {
152         def xpath = '/xpath'
153         def dataNode = new DataNodeBuilder().withXpath(xpath).build()
154         given: 'persistence service returns data for get data request'
155             mockCpsDataPersistenceService.getDataNode(dataspaceName, anchorName, xpath, fetchDescendantsOption) >> dataNode
156         expect: 'service returns same data if uses same parameters'
157             objectUnderTest.getDataNode(dataspaceName, anchorName, xpath, fetchDescendantsOption) == dataNode
158         where: 'all fetch options are supported'
159             fetchDescendantsOption << [FetchDescendantsOption.OMIT_DESCENDANTS, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS]
160     }
161
162     def 'Update data node leaves: #scenario.'() {
163         given: 'schema set for given anchor and dataspace references test-tree model'
164             setupSchemaSetMocks('test-tree.yang')
165         when: 'update data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
166             objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
167         then: 'the persistence service method is invoked with correct parameters'
168             1 * mockCpsDataPersistenceService.updateDataLeaves(dataspaceName, anchorName, expectedNodeXpath, leaves)
169         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
170             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
171         and: 'data updated event is sent to notification service'
172             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, parentNodeXpath, Operation.UPDATE, observedTimestamp)
173         where: 'following parameters were used'
174             scenario         | parentNodeXpath | jsonData                        || expectedNodeXpath                   | leaves
175             'top level node' | '/'             | '{"test-tree": {"branch": []}}' || '/test-tree'                        | Collections.emptyMap()
176             'level 2 node'   | '/test-tree'    | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']' | ['name': 'Name']
177     }
178
179     def 'Update list-element data node with : #scenario.'() {
180         given: 'schema set for given anchor and dataspace references bookstore model'
181             setupSchemaSetMocks('bookstore.yang')
182         when: 'update data method is invoked with json data #jsonData and parent node xpath'
183             objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/bookstore/categories[@code=2]',
184                 jsonData, observedTimestamp)
185         then: 'the persistence service method is invoked with correct parameters'
186             thrown(DataValidationException)
187         where: 'following parameters were used'
188             scenario          | jsonData
189             'multiple expectedLeaves' | '{"code": "01","name": "some-name"}'
190             'one leaf'        | '{"name": "some-name"}'
191     }
192
193     def 'Update Bookstore node leaves' () {
194         given: 'a DMI registry model'
195             setupSchemaSetMocks('bookstore.yang')
196         and: 'the expected json string'
197             def jsonData = '{"categories":[{"code":01,"name":"Romance"}]}'
198         when: 'update data method is invoked with json data and parent node xpath'
199             objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName,
200                 '/bookstore', jsonData, observedTimestamp)
201         then: 'the persistence service method is invoked with correct parameters'
202             1 * mockCpsDataPersistenceService.updateDataLeaves(dataspaceName, anchorName,
203                 "/bookstore/categories[@code='01']", ['name':'Romance', 'code': '01'])
204         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
205             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
206         and: 'the data updated event is sent to the notification service'
207             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/bookstore', Operation.UPDATE, observedTimestamp)
208     }
209
210     def 'Replace data node using singular data node: #scenario.'() {
211         given: 'schema set for given anchor and dataspace references test-tree model'
212             setupSchemaSetMocks('test-tree.yang')
213         when: 'replace data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
214             objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
215         then: 'the persistence service method is invoked with correct parameters'
216             1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
217                 { dataNode -> dataNode.xpath[0] == expectedNodeXpath })
218         and: 'data updated event is sent to notification service'
219             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, parentNodeXpath, Operation.UPDATE, observedTimestamp)
220         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
221             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
222         where: 'following parameters were used'
223             scenario         | parentNodeXpath | jsonData                        || expectedNodeXpath
224             'top level node' | '/'             | '{"test-tree": {"branch": []}}' || '/test-tree'
225             'level 2 node'   | '/test-tree'    | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']'
226     }
227
228     def 'Replace data node using multiple data nodes: #scenario.'() {
229         given: 'schema set for given anchor and dataspace references test-tree model'
230             setupSchemaSetMocks('test-tree.yang')
231         when: 'replace data method is invoked with a map of xpaths and json data'
232             objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, nodesJsonData, observedTimestamp)
233         then: 'the persistence service method is invoked with correct parameters'
234             1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
235                 { dataNode -> dataNode.xpath == expectedNodeXpath})
236         and: 'data updated event is sent to notification service'
237             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, nodesJsonData.keySet()[0], Operation.UPDATE, observedTimestamp)
238             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, nodesJsonData.keySet()[1], Operation.UPDATE, observedTimestamp)
239         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
240             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
241         where: 'following parameters were used'
242             scenario         | nodesJsonData                                                                                                        || expectedNodeXpath
243             'top level node' | ['/' : '{"test-tree": {"branch": []}}', '/test-tree' : '{"branch": [{"name":"Name"}]}']                              || ["/test-tree", "/test-tree/branch[@name='Name']"]
244             'level 2 node'   | ['/test-tree' : '{"branch": [{"name":"Name"}]}', '/test-tree/branch[@name=\'Name\']':'{"nest":{"name":"nestName"}}'] || ["/test-tree/branch[@name='Name']", "/test-tree/branch[@name='Name']/nest"]
245     }
246
247     def 'Replace list content data fragment under parent node.'() {
248         given: 'schema set for given anchor and dataspace references test-tree model'
249             setupSchemaSetMocks('test-tree.yang')
250         when: 'replace list data method is invoked with list element json data'
251             def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
252             objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
253         then: 'the persistence service method is invoked with correct parameters'
254             1 * mockCpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, '/test-tree',
255                 { dataNodeCollection ->
256                     {
257                         assert dataNodeCollection.size() == 2
258                         assert dataNodeCollection.collect { it.getXpath() }
259                             .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
260                     }
261                 }
262             )
263         and: 'the CpsValidator is called on the dataspaceName and AnchorName twice'
264             2 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
265         and: 'data updated event is sent to notification service'
266             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/test-tree', Operation.UPDATE, observedTimestamp)
267     }
268
269     def 'Replace whole list content with empty list element.'() {
270         given: 'schema set for given anchor and dataspace references test-tree model'
271             setupSchemaSetMocks('test-tree.yang')
272         when: 'replace list data method is invoked with empty list'
273             def jsonData = '{"branch": []}'
274             objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
275         then: 'invalid data exception is thrown'
276             thrown(DataValidationException)
277     }
278
279     def 'Delete list element under existing node.'() {
280         given: 'schema set for given anchor and dataspace references test-tree model'
281             setupSchemaSetMocks('test-tree.yang')
282         when: 'delete list data method is invoked with list element json data'
283             objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp)
284         then: 'the persistence service method is invoked with correct parameters'
285             1 * mockCpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, '/test-tree/branch')
286         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
287             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
288         and: 'data updated event is sent to notification service'
289             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/test-tree/branch', Operation.DELETE, observedTimestamp)
290     }
291
292     def 'Delete data node under anchor and dataspace.'() {
293         given: 'schema set for given anchor and dataspace references test tree model'
294             setupSchemaSetMocks('test-tree.yang')
295         when: 'delete data node method is invoked with correct parameters'
296             objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
297         then: 'the persistence service method is invoked with the correct parameters'
298             1 * mockCpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, '/data-node')
299         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
300             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
301         and: 'data updated event is sent to notification service'
302             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/data-node', Operation.DELETE, observedTimestamp)
303     }
304
305     def 'Delete all data nodes for a given anchor and dataspace.'() {
306         given: 'schema set for given anchor and dataspace references test tree model'
307             setupSchemaSetMocks('test-tree.yang')
308         when: 'delete data node method is invoked with correct parameters'
309             objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
310         then: 'data updated event is sent to notification service before the delete'
311             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/', Operation.DELETE, observedTimestamp)
312         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
313             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
314         and: 'the persistence service method is invoked with the correct parameters'
315             1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName)
316     }
317
318     def setupSchemaSetMocks(String... yangResources) {
319         def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
320         mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
321         def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
322         def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
323         mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
324     }
325
326     def 'start session'() {
327         when: 'start session method is called'
328             objectUnderTest.startSession()
329         then: 'the persistence service method to start session is invoked'
330             1 * mockCpsDataPersistenceService.startSession()
331     }
332
333     def 'close session'(){
334         given: 'session Id from calling the start session method'
335             def sessionId = objectUnderTest.startSession()
336         when: 'close session method is called'
337             objectUnderTest.closeSession(sessionId)
338         then: 'the persistence service method to close session is invoked'
339             1 * mockCpsDataPersistenceService.closeSession(sessionId)
340     }
341
342     def 'lock anchor with no timeout parameter'(){
343         when: 'lock anchor method with no timeout parameter with details of anchor entity to lock'
344             objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName')
345         then: 'the persistence service method to lock anchor is invoked with default timeout'
346             1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName',
347                     'some-anchorName', 300L)
348     }
349
350     def 'lock anchor with timeout parameter'(){
351         when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock'
352             objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName',
353                     'some-anchorName', 250L)
354         then: 'the persistence service method to lock anchor is invoked with the given timeout'
355             1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName',
356                     'some-anchorName', 250L)
357     }
358 }