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