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