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 = '{"bookstore-address":[{"bookstore-name":"Easons","address":"Dublin,Ireland","postal-code":"D02HA21"}]}'
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(['/bookstore-address[@bookstore-name=\'Easons\']'])
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 == 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\']']
319 'json list' | '/test-tree' | '{"branch": [{"name":"Name1"}, {"name":"Name2"}]}' || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"]
322 def 'Replace data node using multiple data nodes: #scenario.'() {
323 given: 'schema set for given anchor and dataspace references test-tree model'
324 setupSchemaSetMocks('test-tree.yang')
325 when: 'replace data method is invoked with a map of xpaths and json data'
326 objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, nodesJsonData, observedTimestamp)
327 then: 'the persistence service method is invoked with correct parameters'
328 1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
329 { dataNode -> dataNode.xpath == expectedNodeXpath})
330 and: 'data updated event is sent to notification service'
331 nodesJsonData.keySet().each {
332 1 * mockNotificationService.processDataUpdatedEvent(anchor, it, Operation.UPDATE, observedTimestamp)
334 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
335 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
336 where: 'following parameters were used'
337 scenario | nodesJsonData || expectedNodeXpath
338 'top level node' | ['/' : '{"test-tree": {"branch": []}}', '/test-tree' : '{"branch": [{"name":"Name"}]}'] || ["/test-tree", "/test-tree/branch[@name='Name']"]
339 '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 'json list' | ['/test-tree' : '{"branch": [{"name":"Name1"}, {"name":"Name2"}]}'] || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"]
343 def 'Replace data node with concurrency exception in persistence layer.'() {
344 given: 'the persistence layer throws an concurrency exception'
345 def originalException = new ConcurrencyException('message', 'details')
346 mockCpsDataPersistenceService.updateDataNodesAndDescendants(*_) >> { throw originalException }
347 setupSchemaSetMocks('test-tree.yang')
348 when: 'attempt to replace data node'
349 objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, ['/' : '{"test-tree": {}}'] , observedTimestamp)
350 then: 'the same exception is thrown up'
351 def thrownUp = thrown(ConcurrencyException)
352 assert thrownUp == originalException
355 def 'Replace list content data fragment under parent node.'() {
356 given: 'schema set for given anchor and dataspace references test-tree model'
357 setupSchemaSetMocks('test-tree.yang')
358 when: 'replace list data method is invoked with list element json data'
359 def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
360 objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
361 then: 'the persistence service method is invoked with correct parameters'
362 1 * mockCpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, '/test-tree',
363 { dataNodeCollection ->
365 assert dataNodeCollection.size() == 2
366 assert dataNodeCollection.collect { it.getXpath() }
367 .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
371 and: 'the CpsValidator is called on the dataspaceName and AnchorName twice'
372 2 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
373 and: 'data updated event is sent to notification service'
374 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree', Operation.UPDATE, observedTimestamp)
377 def 'Replace whole list content with empty list element.'() {
378 given: 'schema set for given anchor and dataspace references test-tree model'
379 setupSchemaSetMocks('test-tree.yang')
380 when: 'replace list data method is invoked with empty list'
381 def jsonData = '{"branch": []}'
382 objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
383 then: 'invalid data exception is thrown'
384 thrown(DataValidationException)
387 def 'Delete list element under existing node.'() {
388 when: 'delete list data method is invoked with list element json data'
389 objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp)
390 then: 'the persistence service method is invoked with correct parameters'
391 1 * mockCpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, '/test-tree/branch')
392 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
393 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
394 and: 'data updated event is sent to notification service'
395 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree/branch', Operation.DELETE, observedTimestamp)
398 def 'Delete multiple list elements under existing node.'() {
399 when: 'delete multiple list data method is invoked with list element json data'
400 objectUnderTest.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'], observedTimestamp)
401 then: 'the persistence service method is invoked with correct parameters'
402 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'])
403 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
404 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
405 and: 'two data updated events are sent to notification service'
406 2 * mockNotificationService.processDataUpdatedEvent(anchor, _, Operation.DELETE, observedTimestamp)
409 def 'Delete data node under anchor and dataspace.'() {
410 when: 'delete data node method is invoked with correct parameters'
411 objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
412 then: 'the persistence service method is invoked with the correct parameters'
413 1 * mockCpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, '/data-node')
414 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
415 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
416 and: 'data updated event is sent to notification service'
417 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/data-node', Operation.DELETE, observedTimestamp)
420 def 'Delete all data nodes for a given anchor and dataspace.'() {
421 when: 'delete data nodes method is invoked with correct parameters'
422 objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
423 then: 'data updated event is sent to notification service before the delete'
424 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.DELETE, observedTimestamp)
425 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
426 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
427 and: 'the persistence service method is invoked with the correct parameters'
428 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName)
431 def 'Delete all data nodes for a given anchor and dataspace with batch exception in persistence layer.'() {
432 given: 'a batch exception in persistence layer'
433 def originalException = new DataNodeNotFoundExceptionBatch('ds1','a1',[])
434 mockCpsDataPersistenceService.deleteDataNodes(*_) >> { throw originalException }
435 when: 'attempt to delete data nodes'
436 objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
437 then: 'the original exception is thrown up'
438 def thrownUp = thrown(DataNodeNotFoundExceptionBatch)
439 assert thrownUp == originalException
440 and: 'the exception details contain the expected data'
441 assert thrownUp.details.contains('ds1')
442 assert thrownUp.details.contains('a1')
445 def 'Delete all data nodes for given dataspace and multiple anchors.'() {
446 given: 'schema set for given anchors and dataspace references test tree model'
447 setupSchemaSetMocks('test-tree.yang')
448 mockCpsAdminService.getAnchors(dataspaceName, ['anchor1', 'anchor2']) >>
449 [new Anchor(name: 'anchor1', dataspaceName: dataspaceName),
450 new Anchor(name: 'anchor2', dataspaceName: dataspaceName)]
451 when: 'delete data node method is invoked with correct parameters'
452 objectUnderTest.deleteDataNodes(dataspaceName, ['anchor1', 'anchor2'], observedTimestamp)
453 then: 'data updated events are sent to notification service before the delete'
454 2 * mockNotificationService.processDataUpdatedEvent(_, '/', Operation.DELETE, observedTimestamp)
455 and: 'the CpsValidator is called on the dataspace name and the anchor names'
456 2 * mockCpsValidator.validateNameCharacters(_)
457 and: 'the persistence service method is invoked with the correct parameters'
458 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, _ as Collection<String>)
461 def 'Start session.'() {
462 when: 'start session method is called'
463 objectUnderTest.startSession()
464 then: 'the persistence service method to start session is invoked'
465 1 * mockCpsDataPersistenceService.startSession()
468 def 'Start session with Session Manager Exceptions.'() {
469 given: 'the persistence layer throws an Session Manager Exception'
470 mockCpsDataPersistenceService.startSession() >> { throw originalException }
471 when: 'attempt to start session'
472 objectUnderTest.startSession()
473 then: 'the original exception is thrown up'
474 def thrownUp = thrown(SessionManagerException)
475 assert thrownUp == originalException
476 where: 'variations of Session Manager Exception are used'
477 originalException << [ new SessionManagerException('message','details'),
478 new SessionManagerException('message','details', new Exception('cause')),
479 new SessionTimeoutException('message','details', new Exception('cause'))]
482 def 'Close session.'(){
483 given: 'session Id from calling the start session method'
484 def sessionId = objectUnderTest.startSession()
485 when: 'close session method is called'
486 objectUnderTest.closeSession(sessionId)
487 then: 'the persistence service method to close session is invoked'
488 1 * mockCpsDataPersistenceService.closeSession(sessionId)
491 def 'Lock anchor with no timeout parameter.'(){
492 when: 'lock anchor method with no timeout parameter with details of anchor entity to lock'
493 objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName')
494 then: 'the persistence service method to lock anchor is invoked with default timeout'
495 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 300L)
498 def 'Lock anchor with timeout parameter.'(){
499 when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock'
500 objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
501 then: 'the persistence service method to lock anchor is invoked with the given timeout'
502 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
505 def setupSchemaSetMocks(String... yangResources) {
506 def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
507 mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
508 def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
509 def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
510 mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext