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 * ================================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
19 * SPDX-License-Identifier: Apache-2.0
20 * ============LICENSE_END=========================================================
23 package org.onap.cps.api.impl
25 import org.onap.cps.TestUtils
26 import org.onap.cps.api.CpsAdminService
27 import org.onap.cps.notification.NotificationService
28 import org.onap.cps.notification.Operation
29 import org.onap.cps.spi.CpsDataPersistenceService
30 import org.onap.cps.spi.FetchDescendantsOption
31 import org.onap.cps.spi.exceptions.DataValidationException
32 import org.onap.cps.spi.model.Anchor
33 import org.onap.cps.spi.model.DataNode
34 import org.onap.cps.spi.model.DataNodeBuilder
35 import org.onap.cps.yang.YangTextSchemaSourceSet
36 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
37 import spock.lang.Specification
39 import java.time.OffsetDateTime
41 class CpsDataServiceImplSpec extends Specification {
42 def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService)
43 def mockCpsAdminService = Mock(CpsAdminService)
44 def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)
45 def mockNotificationService = Mock(NotificationService)
47 def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAdminService,
48 mockYangTextSchemaSourceSetCache, mockNotificationService)
51 mockCpsAdminService.getAnchor(dataspaceName, anchorName) >> anchor
54 def dataspaceName = 'some-dataspace'
55 def anchorName = 'some-anchor'
56 def schemaSetName = 'some-schema-set'
57 def anchor = Anchor.builder().name(anchorName).schemaSetName(schemaSetName).build()
58 def observedTimestamp = OffsetDateTime.now()
60 def 'Saving json data.'() {
61 given: 'schema set for given anchor and dataspace references test-tree model'
62 setupSchemaSetMocks('test-tree.yang')
63 when: 'save data method is invoked with test-tree json data'
64 def jsonData = TestUtils.getResourceFileContent('test-tree.json')
65 objectUnderTest.saveData(dataspaceName, anchorName, jsonData, observedTimestamp)
66 then: 'the persistence service method is invoked with correct parameters'
67 1 * mockCpsDataPersistenceService.storeDataNode(dataspaceName, anchorName,
68 { dataNode -> dataNode.xpath == '/test-tree' })
69 and: 'data updated event is sent to notification service'
70 1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/', Operation.CREATE)
73 def 'Saving json data with invalid #scenario.'() {
74 when: 'save data method is invoked with invalid #scenario'
75 objectUnderTest.saveData(dataspaceName, anchorName, _ as String, observedTimestamp)
76 then: 'a data validation exception is thrown'
77 thrown(DataValidationException)
78 and: 'the persistence service method is not invoked'
79 0 * mockCpsDataPersistenceService.storeDataNode(_, _, _)
80 and: 'data updated event is not sent to notification service'
81 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _)
82 where: 'the following parameters are used'
83 scenario | dataspaceName | anchorName
84 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
85 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
86 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
89 def 'Saving child data fragment under existing node.'() {
90 given: 'schema set for given anchor and dataspace references test-tree model'
91 setupSchemaSetMocks('test-tree.yang')
92 when: 'save data method is invoked with test-tree json data'
93 def jsonData = '{"branch": [{"name": "New"}]}'
94 objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
95 then: 'the persistence service method is invoked with correct parameters'
96 1 * mockCpsDataPersistenceService.addChildDataNode(dataspaceName, anchorName, '/test-tree',
97 { dataNode -> dataNode.xpath == '/test-tree/branch[@name=\'New\']' })
98 and: 'data updated event is sent to notification service'
99 1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/test-tree', Operation.CREATE)
102 def 'Saving child data fragment under existing node with invalid #scenario.'() {
103 when: 'save data method is invoked with test-tree and an invalid #scenario'
104 objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', _ as String, observedTimestamp)
105 then: 'a data validation exception is thrown'
106 thrown(DataValidationException)
107 and: 'the persistence service method is not invoked'
108 0 * mockCpsDataPersistenceService.addChildDataNode(_, _, _,_)
109 and: 'data updated event is not sent to notification service'
110 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _)
111 where: 'the following parameters are used'
112 scenario | dataspaceName | anchorName
113 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
114 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
115 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
118 def 'Saving list element 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 list element json data'
122 def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
123 objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
124 then: 'the persistence service method is invoked with correct parameters'
125 1 * mockCpsDataPersistenceService.addListElements(dataspaceName, anchorName, '/test-tree',
126 { dataNodeCollection ->
128 assert dataNodeCollection.size() == 2
129 assert dataNodeCollection.collect { it.getXpath() }
130 .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
134 and: 'data updated event is sent to notification service'
135 1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/test-tree', Operation.UPDATE)
138 def 'Saving empty list element data fragment.'() {
139 given: 'schema set for given anchor and dataspace references test-tree model'
140 setupSchemaSetMocks('test-tree.yang')
141 when: 'save data method is invoked with an empty list'
142 def jsonData = '{"branch": []}'
143 objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
144 then: 'invalid data exception is thrown'
145 thrown(DataValidationException)
148 def 'Saving list element data fragment with invalid #scenario.'() {
149 when: 'save data method is invoked with an invalid #scenario'
150 objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', _ as String, observedTimestamp)
151 then: 'a data validation exception is thrown'
152 thrown(DataValidationException)
153 and: 'add list elements persistence method is not invoked'
154 0 * mockCpsDataPersistenceService.addListElements(_, _, _, _)
155 where: 'the following parameters are used'
156 scenario | dataspaceName | anchorName
157 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
158 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
159 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
162 def 'Get data node with option #fetchDescendantsOption.'() {
164 def dataNode = new DataNodeBuilder().withXpath(xpath).build()
165 given: 'persistence service returns data for get data request'
166 mockCpsDataPersistenceService.getDataNode(dataspaceName, anchorName, xpath, fetchDescendantsOption) >> dataNode
167 expect: 'service returns same data if uses same parameters'
168 objectUnderTest.getDataNode(dataspaceName, anchorName, xpath, fetchDescendantsOption) == dataNode
169 where: 'all fetch options are supported'
170 fetchDescendantsOption << FetchDescendantsOption.values()
173 def 'Get data node with option invalid #scenario.'() {
174 when: 'get data node is invoked with #scenario'
175 objectUnderTest.getDataNode(dataspaceName, anchorName, '/test-tree', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
176 then: 'a data validation exception is thrown'
177 thrown(DataValidationException)
178 and: 'get data node persistence service is not invoked'
179 0 * mockCpsDataPersistenceService.getDataNode(_, _, _, _)
180 where: 'the following parameters are used'
181 scenario | dataspaceName | anchorName
182 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
183 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
184 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
187 def 'Update data node leaves: #scenario.'() {
188 given: 'schema set for given anchor and dataspace references test-tree model'
189 setupSchemaSetMocks('test-tree.yang')
190 when: 'update data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
191 objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
192 then: 'the persistence service method is invoked with correct parameters'
193 1 * mockCpsDataPersistenceService.updateDataLeaves(dataspaceName, anchorName, expectedNodeXpath, leaves)
194 and: 'data updated event is sent to notification service'
195 1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, parentNodeXpath, Operation.UPDATE)
196 where: 'following parameters were used'
197 scenario | parentNodeXpath | jsonData || expectedNodeXpath | leaves
198 'top level node' | '/' | '{"test-tree": {"branch": []}}' || '/test-tree' | Collections.emptyMap()
199 'level 2 node' | '/test-tree' | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']' | ['name': 'Name']
202 def 'Update data node with invalid #scenario.'() {
203 when: 'update data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
204 objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/', '{"test-tree": {"branch": []}}', observedTimestamp)
205 then: 'a data validation exception is thrown'
206 thrown(DataValidationException)
207 and: 'the persistence service method is not invoked'
208 0 * mockCpsDataPersistenceService.updateDataLeaves(_, _, _, _)
209 and: 'data updated event is not sent to notification service'
210 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _)
211 where: 'the following parameters are used'
212 scenario | dataspaceName | anchorName
213 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
214 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
215 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
218 def 'Update list-element data node with : #scenario.'() {
219 given: 'schema set for given anchor and dataspace references bookstore model'
220 setupSchemaSetMocks('bookstore.yang')
221 when: 'update data method is invoked with json data #jsonData and parent node xpath'
222 objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/bookstore/categories[@code=2]',
223 jsonData, observedTimestamp)
224 then: 'the persistence service method is invoked with correct parameters'
225 thrown(DataValidationException)
226 where: 'following parameters were used'
228 'multiple expectedLeaves' | '{"code": "01","name": "some-name"}'
229 'one leaf' | '{"name": "some-name"}'
232 def 'Update Bookstore node leaves' () {
233 given: 'a DMI registry model'
234 setupSchemaSetMocks('bookstore.yang')
235 and: 'the expected json string'
236 def jsonData = '{"categories":[{"code":01,"name":"Romance"}]}'
237 when: 'update data method is invoked with json data and parent node xpath'
238 objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName,
239 '/bookstore', jsonData, observedTimestamp)
240 then: 'the persistence service method is invoked with correct parameters'
241 1 * mockCpsDataPersistenceService.updateDataLeaves(dataspaceName, anchorName,
242 "/bookstore/categories[@code='01']", ['name':'Romance', 'code': '01'])
243 and: 'the data updated event is sent to the notification service'
244 1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/bookstore', Operation.UPDATE)
247 def 'Update Bookstore node leaves with invalid #scenario' () {
248 when: 'update data method is invoked with an invalid #scenario'
249 objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName,
250 '/bookstore', _ as String, observedTimestamp)
251 then: 'a data validation exception is thrown'
252 thrown(DataValidationException)
253 and: 'the persistence service method is not invoked'
254 0 * mockCpsDataPersistenceService.updateDataLeaves(_, _, _, _)
255 and: 'the data updated event is not sent to the notification service'
256 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _)
257 where: 'the following parameters are used'
258 scenario | dataspaceName | anchorName
259 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
260 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
261 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
265 def 'Replace data node: #scenario.'() {
266 given: 'schema set for given anchor and dataspace references test-tree model'
267 setupSchemaSetMocks('test-tree.yang')
268 when: 'replace data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
269 objectUnderTest.replaceNodeTree(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
270 then: 'the persistence service method is invoked with correct parameters'
271 1 * mockCpsDataPersistenceService.replaceDataNodeTree(dataspaceName, anchorName,
272 { dataNode -> dataNode.xpath == expectedNodeXpath })
273 and: 'data updated event is sent to notification service'
274 1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, parentNodeXpath, Operation.UPDATE)
275 where: 'following parameters were used'
276 scenario | parentNodeXpath | jsonData || expectedNodeXpath
277 'top level node' | '/' | '{"test-tree": {"branch": []}}' || '/test-tree'
278 'level 2 node' | '/test-tree' | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']'
281 def 'Replace data node with invalid #scenario.'() {
282 when: 'replace data method is invoked with invalid #scenario'
283 objectUnderTest.replaceNodeTree(dataspaceName, anchorName, '/', _ as String, observedTimestamp)
284 then: 'a data validation exception is thrown'
285 thrown(DataValidationException)
286 and: 'the persistence service method is not invoked'
287 0 * mockCpsDataPersistenceService.replaceDataNodeTree(_, _,_)
288 and: 'data updated event is not sent to notification service'
289 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _)
290 where: 'the following parameters are used'
291 scenario | dataspaceName | anchorName
292 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
293 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
294 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
297 def 'Replace list content data fragment under parent node.'() {
298 given: 'schema set for given anchor and dataspace references test-tree model'
299 setupSchemaSetMocks('test-tree.yang')
300 when: 'replace list data method is invoked with list element json data'
301 def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
302 objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
303 then: 'the persistence service method is invoked with correct parameters'
304 1 * mockCpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, '/test-tree',
305 { dataNodeCollection ->
307 assert dataNodeCollection.size() == 2
308 assert dataNodeCollection.collect { it.getXpath() }
309 .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
313 and: 'data updated event is sent to notification service'
314 1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/test-tree', Operation.UPDATE)
317 def 'Replace whole list content with empty list element.'() {
318 given: 'schema set for given anchor and dataspace references test-tree model'
319 setupSchemaSetMocks('test-tree.yang')
320 when: 'replace list data method is invoked with empty list'
321 def jsonData = '{"branch": []}'
322 objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
323 then: 'invalid data exception is thrown'
324 thrown(DataValidationException)
327 def 'Replace whole list content with an invalid #scenario.'() {
328 when: 'replace list data method is invoked with invalid #scenario'
329 objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', _ as Collection<DataNode>, observedTimestamp)
330 then: 'a data validation exception is thrown'
331 thrown(DataValidationException)
332 and: 'the persistence service method is not invoked'
333 0 * mockCpsDataPersistenceService.replaceListContent(_, _,_)
334 and: 'data updated event is not sent to notification service'
335 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _)
336 where: 'the following parameters are used'
337 scenario | dataspaceName | anchorName
338 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
339 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
340 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
343 def 'Delete list element under existing node.'() {
344 given: 'schema set for given anchor and dataspace references test-tree model'
345 setupSchemaSetMocks('test-tree.yang')
346 when: 'delete list data method is invoked with list element json data'
347 objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp)
348 then: 'the persistence service method is invoked with correct parameters'
349 1 * mockCpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, '/test-tree/branch')
350 and: 'data updated event is sent to notification service'
351 1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/test-tree/branch', Operation.DELETE)
355 def 'Delete list element with an invalid #scenario.'() {
356 when: 'delete list data method is invoked with with invalid #scenario'
357 objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
358 then: 'a data validation exception is thrown'
359 thrown(DataValidationException)
360 and: 'the persistence service method is not invoked'
361 0 * mockCpsDataPersistenceService.deleteListDataNode(_, _, _)
362 and: 'data updated event is not sent to notification service'
363 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _)
364 where: 'the following parameters are used'
365 scenario | dataspaceName | anchorName
366 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
367 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
368 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
371 def 'Delete data node under anchor and dataspace.'() {
372 given: 'schema set for given anchor and dataspace references test tree model'
373 setupSchemaSetMocks('test-tree.yang')
374 when: 'delete data node method is invoked with correct parameters'
375 objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
376 then: 'the persistence service method is invoked with the correct parameters'
377 1 * mockCpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, '/data-node')
378 and: 'data updated event is sent to notification service'
379 1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/data-node', Operation.DELETE)
382 def 'Delete data node with an invalid #scenario.'() {
383 when: 'delete data node method is invoked with invalid #scenario'
384 objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
385 then: 'a data validation exception is thrown'
386 thrown(DataValidationException)
387 and: 'the persistence service method is not invoked'
388 0 * mockCpsDataPersistenceService.deleteDataNode(_, _, _)
389 and: 'data updated event is not sent to notification service'
390 0 * mockNotificationService.processDataUpdatedEvent(_, _, _, _)
391 where: 'the following parameters are used'
392 scenario | dataspaceName | anchorName
393 'dataspace name' | 'dataspace names with spaces' | 'anchorName'
394 'anchor name' | 'dataspaceName' | 'anchor name with spaces'
395 'dataspace and anchor name' | 'dataspace name with spaces' | 'anchor name with spaces'
398 def 'Delete all data nodes for a given anchor and dataspace.'() {
399 given: 'schema set for given anchor and dataspace references test tree model'
400 setupSchemaSetMocks('test-tree.yang')
401 when: 'delete data node method is invoked with correct parameters'
402 objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
403 then: 'the persistence service method is invoked with the correct parameters'
404 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName)
405 and: 'data updated event is sent to notification service'
406 1 * mockNotificationService.processDataUpdatedEvent(anchor, observedTimestamp, '/', Operation.DELETE)
410 def setupSchemaSetMocks(String... yangResources) {
411 def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
412 mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
413 def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
414 def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
415 mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
418 def 'start session'() {
419 when: 'start session method is called'
420 objectUnderTest.startSession()
421 then: 'the persistence service method to start session is invoked'
422 1 * mockCpsDataPersistenceService.startSession()
425 def 'close session'(){
426 given: 'session Id from calling the start session method'
427 def sessionId = objectUnderTest.startSession()
428 when: 'close session method is called'
429 objectUnderTest.closeSession(sessionId)
430 then: 'the persistence service method to close session is invoked'
431 1 * mockCpsDataPersistenceService.closeSession(sessionId)
434 def 'lock anchor with no timeout parameter'(){
435 when: 'lock anchor method with no timeout parameter with details of anchor entity to lock'
436 objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName')
437 then: 'the persistence service method to lock anchor is invoked with default timeout'
438 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName',
439 'some-anchorName', 300L)
442 def 'lock anchor with timeout parameter'(){
443 when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock'
444 objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName',
445 'some-anchorName', 250L)
446 then: 'the persistence service method to lock anchor is invoked with the given timeout'
447 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName',
448 'some-anchorName', 250L)