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