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