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-2023 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.ConcurrencyException
33 import org.onap.cps.spi.exceptions.DataNodeNotFoundExceptionBatch
34 import org.onap.cps.spi.exceptions.DataValidationException
35 import org.onap.cps.spi.exceptions.SessionManagerException
36 import org.onap.cps.spi.exceptions.SessionTimeoutException
37 import org.onap.cps.spi.model.Anchor
38 import org.onap.cps.spi.model.DataNode
39 import org.onap.cps.spi.model.DataNodeBuilder
40 import org.onap.cps.utils.ContentType
41 import org.onap.cps.utils.TimedYangParser
42 import org.onap.cps.yang.YangTextSchemaSourceSet
43 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
44 import spock.lang.Specification
45 import org.onap.cps.spi.utils.CpsValidator
47 import java.time.OffsetDateTime
48 import java.util.stream.Collectors
50 class CpsDataServiceImplSpec extends Specification {
51 def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService)
52 def mockCpsAdminService = Mock(CpsAdminService)
53 def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)
54 def mockNotificationService = Mock(NotificationService)
55 def mockCpsValidator = Mock(CpsValidator)
56 def timedYangParser = new TimedYangParser()
58 def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAdminService,
59 mockYangTextSchemaSourceSetCache, mockNotificationService, mockCpsValidator, timedYangParser)
62 mockCpsAdminService.getAnchor(dataspaceName, anchorName) >> anchor
65 def dataspaceName = 'some-dataspace'
66 def anchorName = 'some-anchor'
67 def schemaSetName = 'some-schema-set'
68 def anchor = Anchor.builder().name(anchorName).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build()
69 def observedTimestamp = OffsetDateTime.now()
71 def 'Saving #scenario data.'() {
72 given: 'schema set for given anchor and dataspace references test-tree model'
73 setupSchemaSetMocks('test-tree.yang')
74 when: 'save data method is invoked with test-tree #scenario data'
75 def data = TestUtils.getResourceFileContent(dataFile)
76 objectUnderTest.saveData(dataspaceName, anchorName, data, observedTimestamp, contentType)
77 then: 'the persistence service method is invoked with correct parameters'
78 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
79 { dataNode -> dataNode.xpath[0] == '/test-tree' })
80 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
81 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
82 and: 'data updated event is sent to notification service'
83 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.CREATE, observedTimestamp)
84 where: 'given parameters'
85 scenario | dataFile | contentType
86 'json' | 'test-tree.json' | ContentType.JSON
87 'xml' | 'test-tree.xml' | ContentType.XML
90 def 'Saving data with error: #scenario.'() {
91 given: 'schema set for given anchor and dataspace references test-tree model'
92 setupSchemaSetMocks('test-tree.yang')
93 when: 'save data method is invoked with test-tree json data'
94 objectUnderTest.saveData(dataspaceName, anchorName, invalidData, observedTimestamp, contentType)
95 then: 'a data validation exception is thrown with the correct message'
96 def exceptionThrown = thrown(DataValidationException)
97 assert exceptionThrown.message.startsWith(expectedMessage)
98 where: 'given parameters'
99 scenario | invalidData | contentType || expectedMessage
100 'no data nodes' | '{}' | ContentType.JSON || 'No data nodes'
101 'invalid json' | '{invalid json' | ContentType.JSON || 'Failed to parse json data'
102 'invalid xml' | '<invalid xml' | ContentType.XML || 'Failed to parse xml data'
105 def 'Saving #scenarioDesired data exception during notification.'() {
106 given: 'schema set for given anchor and dataspace references test-tree model'
107 setupSchemaSetMocks('test-tree.yang')
108 and: 'the notification service throws an exception'
109 mockNotificationService.processDataUpdatedEvent(*_) >> { throw new RuntimeException('to be ignored')}
110 when: 'save data method is invoked with test-tree json data'
111 def data = TestUtils.getResourceFileContent('test-tree.json')
112 objectUnderTest.saveData(dataspaceName, anchorName, data, observedTimestamp)
113 then: 'the exception is ignored'
117 def 'Saving list element data fragment under Root node.'() {
118 given: 'schema set for given anchor and dataspace references bookstore model'
119 setupSchemaSetMocks('bookstore.yang')
120 when: 'save data method is invoked with list element json data'
121 def jsonData = '{"multiple-data-tree:invoice": [{"ProductID": "2","ProductName": "Banana","price": "100","stock": True}]}'
122 objectUnderTest.saveListElements(dataspaceName, anchorName, '/', jsonData, observedTimestamp)
123 then: 'the persistence service method is invoked with correct parameters'
124 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
125 { dataNodeCollection ->
127 assert dataNodeCollection.size() == 1
128 assert dataNodeCollection.collect { it.getXpath() }
129 .containsAll(['/invoice[@ProductID=\'2\']'])
133 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
134 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
135 and: 'data updated event is sent to notification service'
136 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.UPDATE, observedTimestamp)
139 def 'Saving child data fragment under existing node.'() {
140 given: 'schema set for given anchor and dataspace references test-tree model'
141 setupSchemaSetMocks('test-tree.yang')
142 when: 'save data method is invoked with test-tree json data'
143 def jsonData = '{"branch": [{"name": "New"}]}'
144 objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
145 then: 'the persistence service method is invoked with correct parameters'
146 1 * mockCpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, '/test-tree',
147 { dataNode -> dataNode.xpath[0] == '/test-tree/branch[@name=\'New\']' })
148 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
149 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
150 and: 'data updated event is sent to notification service'
151 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree', Operation.CREATE, observedTimestamp)
154 def 'Saving list element data fragment under existing node.'() {
155 given: 'schema set for given anchor and dataspace references test-tree model'
156 setupSchemaSetMocks('test-tree.yang')
157 when: 'save data method is invoked with list element json data'
158 def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
159 objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
160 then: 'the persistence service method is invoked with correct parameters'
161 1 * mockCpsDataPersistenceService.addListElements(dataspaceName, anchorName, '/test-tree',
162 { dataNodeCollection ->
164 assert dataNodeCollection.size() == 2
165 assert dataNodeCollection.collect { it.getXpath() }
166 .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
170 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
171 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
172 and: 'data updated event is sent to notification service'
173 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree', Operation.UPDATE, observedTimestamp)
176 def 'Saving collection of a batch with data fragment under existing node.'() {
177 given: 'schema set for given anchor and dataspace references test-tree model'
178 setupSchemaSetMocks('test-tree.yang')
179 when: 'save data method is invoked with list element json data'
180 def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
181 objectUnderTest.saveListElementsBatch(dataspaceName, anchorName, '/test-tree', [jsonData], observedTimestamp)
182 then: 'the persistence service method is invoked with correct parameters'
183 1 * mockCpsDataPersistenceService.addMultipleLists(dataspaceName, anchorName, '/test-tree',_) >> {
185 def listElementsCollection = args[3] as Collection<Collection<DataNode>>
186 assert listElementsCollection.size() == 1
187 def listOfXpaths = listElementsCollection.stream().flatMap(x -> x.stream()).map(it-> it.xpath).collect(Collectors.toList())
188 assert listOfXpaths.size() == 2
189 assert listOfXpaths.containsAll(['/test-tree/branch[@name=\'B\']','/test-tree/branch[@name=\'A\']'])
192 and: 'data updated event is sent to notification service'
193 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree', Operation.UPDATE, observedTimestamp)
196 def 'Saving empty list element data fragment.'() {
197 given: 'schema set for given anchor and dataspace references test-tree model'
198 setupSchemaSetMocks('test-tree.yang')
199 when: 'save data method is invoked with an empty list'
200 def jsonData = '{"branch": []}'
201 objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
202 then: 'invalid data exception is thrown'
203 thrown(DataValidationException)
206 def 'Get all data nodes #scenario.'() {
207 given: 'persistence service returns data for GET request'
208 mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption) >> dataNode
209 expect: 'service returns same data if using same parameters'
210 objectUnderTest.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption) == dataNode
211 where: 'following parameters were used'
212 scenario | xpath | fetchDescendantsOption | dataNode
213 'with root node xpath and descendants' | '/' | FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath-1').build(), new DataNodeBuilder().withXpath('/xpath-2').build()]
214 'with root node xpath and no descendants' | '/' | FetchDescendantsOption.OMIT_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath-1').build(), new DataNodeBuilder().withXpath('/xpath-2').build()]
215 'with valid xpath and descendants' | '/xpath'| FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath').build()]
216 'with valid xpath and no descendants' | '/xpath'| FetchDescendantsOption.OMIT_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath').build()]
219 def 'Get all data nodes over multiple xpaths with option #fetchDescendantsOption.'() {
220 def xpath1 = '/xpath-1'
221 def xpath2 = '/xpath-2'
222 def dataNode = [new DataNodeBuilder().withXpath(xpath1).build(), new DataNodeBuilder().withXpath(xpath2).build()]
223 given: 'persistence service returns data for get data request'
224 mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, anchorName, [xpath1, xpath2], fetchDescendantsOption) >> dataNode
225 expect: 'service returns same data if uses same parameters'
226 objectUnderTest.getDataNodesForMultipleXpaths(dataspaceName, anchorName, [xpath1, xpath2], fetchDescendantsOption) == dataNode
227 where: 'all fetch options are supported'
228 fetchDescendantsOption << [FetchDescendantsOption.OMIT_DESCENDANTS, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS]
231 def 'Update data node leaves: #scenario.'() {
232 given: 'schema set for given anchor and dataspace references test-tree model'
233 setupSchemaSetMocks('test-tree.yang')
234 when: 'update data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
235 objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
236 then: 'the persistence service method is invoked with correct parameters'
237 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, {dataNode -> dataNode.keySet()[0] == expectedNodeXpath})
238 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
239 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
240 and: 'data updated event is sent to notification service'
241 1 * mockNotificationService.processDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp)
242 where: 'following parameters were used'
243 scenario | parentNodeXpath | jsonData || expectedNodeXpath
244 'top level node' | '/' | '{"test-tree": {"branch": []}}' || '/test-tree'
245 'level 2 node' | '/test-tree' | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']'
248 def 'Update list-element data node with : #scenario.'() {
249 given: 'schema set for given anchor and dataspace references bookstore model'
250 setupSchemaSetMocks('bookstore.yang')
251 when: 'update data method is invoked with json data #jsonData and parent node xpath'
252 objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/bookstore/categories[@code=2]',
253 jsonData, observedTimestamp)
254 then: 'the persistence service method is invoked with correct parameters'
255 thrown(DataValidationException)
256 where: 'following parameters were used'
258 'multiple expectedLeaves' | '{"code": "01","name": "some-name"}'
259 'one leaf' | '{"name": "some-name"}'
262 def 'Update data nodes in different containers.' () {
263 given: 'schema set for given dataspace and anchor refers multipleDataTree model'
264 setupSchemaSetMocks('multipleDataTree.yang')
265 and: 'json string with multiple data trees'
266 def parentNodeXpath = '/'
267 def updatedJsonData = '{"first-container":{"a-leaf":"a-new-Value"},"last-container":{"x-leaf":"x-new-value"}}'
268 when: 'update operation is performed on multiple data nodes'
269 objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, updatedJsonData, observedTimestamp)
270 then: 'the persistence service method is invoked with correct parameters'
271 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, {dataNode -> dataNode.keySet()[index] == expectedNodeXpath})
272 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
273 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
274 and: 'data updated event is sent to notification service'
275 1 * mockNotificationService.processDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp)
276 where: 'the following parameters were used'
277 index | expectedNodeXpath
278 0 | '/first-container'
279 1 | '/last-container'
282 def 'Update Bookstore node leaves and child.' () {
283 given: 'a DMI registry model'
284 setupSchemaSetMocks('bookstore.yang')
285 and: 'json update for a category (parent) and new book (child)'
286 def jsonData = '{"categories":[{"code":01,"name":"Romance","books": [{"title": "new"}]}]}'
287 when: 'update data method is invoked with json data and parent node xpath'
288 objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName, '/bookstore', jsonData, observedTimestamp)
289 then: 'the persistence service method is invoked for the category (parent)'
290 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName,
291 {updatedDataNodesPerXPath -> updatedDataNodesPerXPath.keySet()
292 .iterator().next() == "/bookstore/categories[@code='01']"})
293 and: 'the persistence service method is invoked for the new book (child)'
294 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName,
295 {updatedDataNodesPerXPath -> updatedDataNodesPerXPath.keySet()
296 .iterator().next() == "/bookstore/categories[@code='01']/books[@title='new']"})
297 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
298 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
299 and: 'the data updated event is sent to the notification service'
300 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/bookstore', Operation.UPDATE, observedTimestamp)
303 def 'Replace data node using singular data node: #scenario.'() {
304 given: 'schema set for given anchor and dataspace references test-tree model'
305 setupSchemaSetMocks('test-tree.yang')
306 when: 'replace data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
307 objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
308 then: 'the persistence service method is invoked with correct parameters'
309 1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
310 { dataNode -> dataNode.xpath[0] == expectedNodeXpath })
311 and: 'data updated event is sent to notification service'
312 1 * mockNotificationService.processDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp)
313 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
314 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
315 where: 'following parameters were used'
316 scenario | parentNodeXpath | jsonData || expectedNodeXpath
317 'top level node' | '/' | '{"test-tree": {"branch": []}}' || '/test-tree'
318 'level 2 node' | '/test-tree' | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']'
321 def 'Replace data node using multiple data nodes: #scenario.'() {
322 given: 'schema set for given anchor and dataspace references test-tree model'
323 setupSchemaSetMocks('test-tree.yang')
324 when: 'replace data method is invoked with a map of xpaths and json data'
325 objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, nodesJsonData, observedTimestamp)
326 then: 'the persistence service method is invoked with correct parameters'
327 1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
328 { dataNode -> dataNode.xpath == expectedNodeXpath})
329 and: 'data updated event is sent to notification service'
330 1 * mockNotificationService.processDataUpdatedEvent(anchor, nodesJsonData.keySet()[0], Operation.UPDATE, observedTimestamp)
331 1 * mockNotificationService.processDataUpdatedEvent(anchor, nodesJsonData.keySet()[1], Operation.UPDATE, observedTimestamp)
332 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
333 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
334 where: 'following parameters were used'
335 scenario | nodesJsonData || expectedNodeXpath
336 'top level node' | ['/' : '{"test-tree": {"branch": []}}', '/test-tree' : '{"branch": [{"name":"Name"}]}'] || ["/test-tree", "/test-tree/branch[@name='Name']"]
337 '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"]
340 def 'Replace data node with concurrency exception in persistence layer.'() {
341 given: 'the persistence layer throws an concurrency exception'
342 def originalException = new ConcurrencyException('message', 'details')
343 mockCpsDataPersistenceService.updateDataNodesAndDescendants(*_) >> { throw originalException }
344 setupSchemaSetMocks('test-tree.yang')
345 when: 'attempt to replace data node'
346 objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, ['/' : '{"test-tree": {}}'] , observedTimestamp)
347 then: 'the same exception is thrown up'
348 def thrownUp = thrown(ConcurrencyException)
349 assert thrownUp == originalException
352 def 'Replace list content data fragment under parent node.'() {
353 given: 'schema set for given anchor and dataspace references test-tree model'
354 setupSchemaSetMocks('test-tree.yang')
355 when: 'replace list data method is invoked with list element json data'
356 def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
357 objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
358 then: 'the persistence service method is invoked with correct parameters'
359 1 * mockCpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, '/test-tree',
360 { dataNodeCollection ->
362 assert dataNodeCollection.size() == 2
363 assert dataNodeCollection.collect { it.getXpath() }
364 .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
368 and: 'the CpsValidator is called on the dataspaceName and AnchorName twice'
369 2 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
370 and: 'data updated event is sent to notification service'
371 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree', Operation.UPDATE, observedTimestamp)
374 def 'Replace whole list content with empty list element.'() {
375 given: 'schema set for given anchor and dataspace references test-tree model'
376 setupSchemaSetMocks('test-tree.yang')
377 when: 'replace list data method is invoked with empty list'
378 def jsonData = '{"branch": []}'
379 objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
380 then: 'invalid data exception is thrown'
381 thrown(DataValidationException)
384 def 'Delete list element under existing node.'() {
385 when: 'delete list data method is invoked with list element json data'
386 objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp)
387 then: 'the persistence service method is invoked with correct parameters'
388 1 * mockCpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, '/test-tree/branch')
389 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
390 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
391 and: 'data updated event is sent to notification service'
392 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree/branch', Operation.DELETE, observedTimestamp)
395 def 'Delete multiple list elements under existing node.'() {
396 when: 'delete multiple list data method is invoked with list element json data'
397 objectUnderTest.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'], observedTimestamp)
398 then: 'the persistence service method is invoked with correct parameters'
399 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'])
400 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
401 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
402 and: 'two data updated events are sent to notification service'
403 2 * mockNotificationService.processDataUpdatedEvent(anchor, _, Operation.DELETE, observedTimestamp)
406 def 'Delete data node under anchor and dataspace.'() {
407 when: 'delete data node method is invoked with correct parameters'
408 objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
409 then: 'the persistence service method is invoked with the correct parameters'
410 1 * mockCpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, '/data-node')
411 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
412 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
413 and: 'data updated event is sent to notification service'
414 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/data-node', Operation.DELETE, observedTimestamp)
417 def 'Delete all data nodes for a given anchor and dataspace.'() {
418 when: 'delete data nodes method is invoked with correct parameters'
419 objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
420 then: 'data updated event is sent to notification service before the delete'
421 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.DELETE, observedTimestamp)
422 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
423 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
424 and: 'the persistence service method is invoked with the correct parameters'
425 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName)
428 def 'Delete all data nodes for a given anchor and dataspace with batch exception in persistence layer.'() {
429 given: 'a batch exception in persistence layer'
430 def originalException = new DataNodeNotFoundExceptionBatch('ds1','a1',[])
431 mockCpsDataPersistenceService.deleteDataNodes(*_) >> { throw originalException }
432 when: 'attempt to delete data nodes'
433 objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
434 then: 'the original exception is thrown up'
435 def thrownUp = thrown(DataNodeNotFoundExceptionBatch)
436 assert thrownUp == originalException
437 and: 'the exception details contain the expected data'
438 assert thrownUp.details.contains('ds1')
439 assert thrownUp.details.contains('a1')
442 def 'Delete all data nodes for given dataspace and multiple anchors.'() {
443 given: 'schema set for given anchors and dataspace references test tree model'
444 setupSchemaSetMocks('test-tree.yang')
445 mockCpsAdminService.getAnchors(dataspaceName, ['anchor1', 'anchor2']) >>
446 [new Anchor(name: 'anchor1', dataspaceName: dataspaceName),
447 new Anchor(name: 'anchor2', dataspaceName: dataspaceName)]
448 when: 'delete data node method is invoked with correct parameters'
449 objectUnderTest.deleteDataNodes(dataspaceName, ['anchor1', 'anchor2'], observedTimestamp)
450 then: 'data updated events are sent to notification service before the delete'
451 2 * mockNotificationService.processDataUpdatedEvent(_, '/', Operation.DELETE, observedTimestamp)
452 and: 'the CpsValidator is called on the dataspace name and the anchor names'
453 2 * mockCpsValidator.validateNameCharacters(_)
454 and: 'the persistence service method is invoked with the correct parameters'
455 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, _ as Collection<String>)
458 def 'Start session.'() {
459 when: 'start session method is called'
460 objectUnderTest.startSession()
461 then: 'the persistence service method to start session is invoked'
462 1 * mockCpsDataPersistenceService.startSession()
465 def 'Start session with Session Manager Exceptions.'() {
466 given: 'the persistence layer throws an Session Manager Exception'
467 mockCpsDataPersistenceService.startSession() >> { throw originalException }
468 when: 'attempt to start session'
469 objectUnderTest.startSession()
470 then: 'the original exception is thrown up'
471 def thrownUp = thrown(SessionManagerException)
472 assert thrownUp == originalException
473 where: 'variations of Session Manager Exception are used'
474 originalException << [ new SessionManagerException('message','details'),
475 new SessionManagerException('message','details', new Exception('cause')),
476 new SessionTimeoutException('message','details', new Exception('cause'))]
479 def 'Close session.'(){
480 given: 'session Id from calling the start session method'
481 def sessionId = objectUnderTest.startSession()
482 when: 'close session method is called'
483 objectUnderTest.closeSession(sessionId)
484 then: 'the persistence service method to close session is invoked'
485 1 * mockCpsDataPersistenceService.closeSession(sessionId)
488 def 'Lock anchor with no timeout parameter.'(){
489 when: 'lock anchor method with no timeout parameter with details of anchor entity to lock'
490 objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName')
491 then: 'the persistence service method to lock anchor is invoked with default timeout'
492 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 300L)
495 def 'Lock anchor with timeout parameter.'(){
496 when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock'
497 objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
498 then: 'the persistence service method to lock anchor is invoked with the given timeout'
499 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
502 def setupSchemaSetMocks(String... yangResources) {
503 def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
504 mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
505 def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
506 def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
507 mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext