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
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.yang.YangTextSchemaSourceSet
38 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
39 import spock.lang.Specification
40 import org.onap.cps.spi.utils.CpsValidator
42 import java.time.OffsetDateTime
43 import java.util.stream.Collectors
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)
52 def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAdminService,
53 mockYangTextSchemaSourceSetCache, mockNotificationService, mockCpsValidator)
56 mockCpsAdminService.getAnchor(dataspaceName, anchorName) >> anchor
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()
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)
80 0 | '/first-container'
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
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
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)
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 ->
143 assert dataNodeCollection.size() == 2
144 assert dataNodeCollection.collect { it.getXpath() }
145 .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
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)
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',_) >> {
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\']'])
171 and: 'data updated event is sent to notification service'
172 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/test-tree', Operation.UPDATE, observedTimestamp)
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)
185 def 'Get data node with option #fetchDescendantsOption.'() {
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]
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']
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'
223 'multiple expectedLeaves' | '{"code": "01","name": "some-name"}'
224 'one leaf' | '{"name": "some-name"}'
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)
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\']'
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"]
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 ->
291 assert dataNodeCollection.size() == 2
292 assert dataNodeCollection.collect { it.getXpath() }
293 .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
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)
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)
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)
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)
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)
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
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()
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)
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)
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)