2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2021-2024 Nordix Foundation
4 * Modifications Copyright (C) 2021 Pantheon.tech
5 * Modifications Copyright (C) 2021-2022 Bell Canada.
6 * Modifications Copyright (C) 2022-2024 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 ch.qos.logback.classic.Level
27 import ch.qos.logback.classic.Logger
28 import ch.qos.logback.core.read.ListAppender
29 import org.onap.cps.TestUtils
30 import org.onap.cps.api.CpsAnchorService
31 import org.onap.cps.api.CpsDeltaService
32 import org.onap.cps.events.CpsDataUpdateEventsService
33 import org.onap.cps.spi.CpsDataPersistenceService
34 import org.onap.cps.spi.FetchDescendantsOption
35 import org.onap.cps.spi.exceptions.ConcurrencyException
36 import org.onap.cps.spi.exceptions.DataNodeNotFoundExceptionBatch
37 import org.onap.cps.spi.exceptions.DataValidationException
38 import org.onap.cps.spi.exceptions.SessionManagerException
39 import org.onap.cps.spi.exceptions.SessionTimeoutException
40 import org.onap.cps.spi.model.Anchor
41 import org.onap.cps.spi.model.DataNodeBuilder
42 import org.onap.cps.spi.utils.CpsValidator
43 import org.onap.cps.utils.ContentType
44 import org.onap.cps.utils.YangParser
45 import org.onap.cps.utils.YangParserHelper
46 import org.onap.cps.yang.YangTextSchemaSourceSet
47 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
48 import org.slf4j.LoggerFactory
49 import org.springframework.context.annotation.AnnotationConfigApplicationContext
50 import spock.lang.Shared
51 import spock.lang.Specification
52 import java.time.OffsetDateTime
54 class CpsDataServiceImplSpec extends Specification {
55 def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService)
56 def mockCpsAnchorService = Mock(CpsAnchorService)
57 def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)
58 def mockCpsValidator = Mock(CpsValidator)
59 def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache)
60 def mockCpsDeltaService = Mock(CpsDeltaService);
61 def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService)
63 def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockDataUpdateEventsService, mockCpsAnchorService, mockCpsValidator, yangParser, mockCpsDeltaService)
65 def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class)
66 def loggingListAppender
67 def applicationContext = new AnnotationConfigApplicationContext()
70 mockCpsAnchorService.getAnchor(dataspaceName, anchorName) >> anchor
71 mockCpsAnchorService.getAnchor(dataspaceName, ANCHOR_NAME_1) >> anchor1
72 mockCpsAnchorService.getAnchor(dataspaceName, ANCHOR_NAME_2) >> anchor2
73 logger.setLevel(Level.DEBUG)
74 loggingListAppender = new ListAppender()
75 logger.addAppender(loggingListAppender)
76 loggingListAppender.start()
77 applicationContext.refresh()
81 ((Logger) LoggerFactory.getLogger(CpsDataServiceImpl.class)).detachAndStopAllAppenders()
82 applicationContext.close()
86 static def ANCHOR_NAME_1 = 'some-anchor-1'
88 static def ANCHOR_NAME_2 = 'some-anchor-2'
89 def dataspaceName = 'some-dataspace'
90 def anchorName = 'some-anchor'
91 def schemaSetName = 'some-schema-set'
92 def anchor = Anchor.builder().name(anchorName).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build()
93 def anchor1 = Anchor.builder().name(ANCHOR_NAME_1).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build()
94 def anchor2 = Anchor.builder().name(ANCHOR_NAME_2).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build()
95 def observedTimestamp = OffsetDateTime.now()
97 def 'Saving #scenario data.'() {
98 given: 'schema set for given anchor and dataspace references test-tree model'
99 setupSchemaSetMocks('test-tree.yang')
100 when: 'save data method is invoked with test-tree #scenario data'
101 def data = TestUtils.getResourceFileContent(dataFile)
102 objectUnderTest.saveData(dataspaceName, anchorName, data, observedTimestamp, contentType)
103 then: 'the persistence service method is invoked with correct parameters'
104 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
105 { dataNode -> dataNode.xpath[0] == '/test-tree' })
106 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
107 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
108 where: 'given parameters'
109 scenario | dataFile | contentType
110 'json' | 'test-tree.json' | ContentType.JSON
111 'xml' | 'test-tree.xml' | ContentType.XML
114 def 'Saving data with error: #scenario.'() {
115 given: 'schema set for given anchor and dataspace references test-tree model'
116 setupSchemaSetMocks('test-tree.yang')
117 when: 'save data method is invoked with test-tree json data'
118 objectUnderTest.saveData(dataspaceName, anchorName, invalidData, observedTimestamp, contentType)
119 then: 'a data validation exception is thrown with the correct message'
120 def exceptionThrown = thrown(DataValidationException)
121 assert exceptionThrown.message.startsWith(expectedMessage)
122 where: 'given parameters'
123 scenario | invalidData | contentType || expectedMessage
124 'no data nodes' | '{}' | ContentType.JSON || 'No data nodes'
125 'invalid json' | '{invalid json' | ContentType.JSON || 'Failed to parse json data'
126 'invalid xml' | '<invalid xml' | ContentType.XML || 'Failed to parse xml data'
129 def 'Saving list element data fragment under Root node.'() {
130 given: 'schema set for given anchor and dataspace references bookstore model'
131 setupSchemaSetMocks('bookstore.yang')
132 when: 'save data method is invoked with list element json data'
133 def jsonData = '{"bookstore-address":[{"bookstore-name":"Easons","address":"Dublin,Ireland","postal-code":"D02HA21"}]}'
134 objectUnderTest.saveListElements(dataspaceName, anchorName, '/', jsonData, observedTimestamp)
135 then: 'the persistence service method is invoked with correct parameters'
136 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
137 { dataNodeCollection ->
139 assert dataNodeCollection.size() == 1
140 assert dataNodeCollection.collect { it.getXpath() }
141 .containsAll(['/bookstore-address[@bookstore-name=\'Easons\']'])
145 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
146 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
149 def 'Saving child data fragment under existing node.'() {
150 given: 'schema set for given anchor and dataspace references test-tree model'
151 setupSchemaSetMocks('test-tree.yang')
152 when: 'save data method is invoked with test-tree json data'
153 def jsonData = '{"branch": [{"name": "New"}]}'
154 objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
155 then: 'the persistence service method is invoked with correct parameters'
156 1 * mockCpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, '/test-tree',
157 { dataNode -> dataNode.xpath[0] == '/test-tree/branch[@name=\'New\']' })
158 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
159 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
162 def 'Saving list element data fragment under existing node.'() {
163 given: 'schema set for given anchor and dataspace references test-tree model'
164 setupSchemaSetMocks('test-tree.yang')
165 when: 'save data method is invoked with list element json data'
166 def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
167 objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
168 then: 'the persistence service method is invoked with correct parameters'
169 1 * mockCpsDataPersistenceService.addListElements(dataspaceName, anchorName, '/test-tree',
170 { dataNodeCollection ->
172 assert dataNodeCollection.size() == 2
173 assert dataNodeCollection.collect { it.getXpath() }
174 .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
178 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
179 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
182 def 'Saving empty list element data fragment.'() {
183 given: 'schema set for given anchor and dataspace references test-tree model'
184 setupSchemaSetMocks('test-tree.yang')
185 when: 'save data method is invoked with an empty list'
186 def jsonData = '{"branch": []}'
187 objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
188 then: 'invalid data exception is thrown'
189 thrown(DataValidationException)
192 def 'Get all data nodes #scenario.'() {
193 given: 'persistence service returns data for GET request'
194 mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption) >> dataNode
195 expect: 'service returns same data if using same parameters'
196 objectUnderTest.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption) == dataNode
197 where: 'following parameters were used'
198 scenario | xpath | fetchDescendantsOption | dataNode
199 'with root node xpath and descendants' | '/' | FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath-1').build(), new DataNodeBuilder().withXpath('/xpath-2').build()]
200 'with root node xpath and no descendants' | '/' | FetchDescendantsOption.OMIT_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath-1').build(), new DataNodeBuilder().withXpath('/xpath-2').build()]
201 'with valid xpath and descendants' | '/xpath'| FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath').build()]
202 'with valid xpath and no descendants' | '/xpath'| FetchDescendantsOption.OMIT_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath').build()]
205 def 'Get all data nodes over multiple xpaths with option #fetchDescendantsOption.'() {
206 def xpath1 = '/xpath-1'
207 def xpath2 = '/xpath-2'
208 def dataNode = [new DataNodeBuilder().withXpath(xpath1).build(), new DataNodeBuilder().withXpath(xpath2).build()]
209 given: 'persistence service returns data for get data request'
210 mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, anchorName, [xpath1, xpath2], fetchDescendantsOption) >> dataNode
211 expect: 'service returns same data if uses same parameters'
212 objectUnderTest.getDataNodesForMultipleXpaths(dataspaceName, anchorName, [xpath1, xpath2], fetchDescendantsOption) == dataNode
213 where: 'all fetch options are supported'
214 fetchDescendantsOption << [FetchDescendantsOption.OMIT_DESCENDANTS, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS]
217 def 'Get delta between 2 anchors'() {
218 given: 'some xpath, source and target data nodes'
220 def sourceDataNodes = [new DataNodeBuilder().withXpath(xpath).build()]
221 def targetDataNodes = [new DataNodeBuilder().withXpath(xpath).build()]
222 when: 'attempt to get delta between 2 anchors'
223 objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
224 then: 'the dataspace and anchor names are validated'
225 2 * mockCpsValidator.validateNameCharacters(_)
226 and: 'data nodes are fetched using appropriate persistence layer method'
227 mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> sourceDataNodes
228 mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_2, [xpath], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> targetDataNodes
229 and: 'appropriate delta service method is invoked once with correct source and target data nodes'
230 1 * mockCpsDeltaService.getDeltaReports(sourceDataNodes, targetDataNodes)
233 def 'Update data node leaves: #scenario.'() {
234 given: 'schema set for given anchor and dataspace references test-tree model'
235 setupSchemaSetMocks('test-tree.yang')
236 when: 'update data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
237 objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
238 then: 'the persistence service method is invoked with correct parameters'
239 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, {dataNode -> dataNode.keySet()[0] == expectedNodeXpath})
240 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
241 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
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 where: 'the following parameters were used'
275 index | expectedNodeXpath
276 0 | '/first-container'
277 1 | '/last-container'
280 def 'Update Bookstore node leaves and child.' () {
281 given: 'a DMI registry model'
282 setupSchemaSetMocks('bookstore.yang')
283 and: 'json update for a category (parent) and new book (child)'
284 def jsonData = '{"categories":[{"code":01,"name":"Romance","books": [{"title": "new"}]}]}'
285 when: 'update data method is invoked with json data and parent node xpath'
286 objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName, '/bookstore', jsonData, observedTimestamp)
287 then: 'the persistence service method is invoked for the category (parent)'
288 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName,
289 {updatedDataNodesPerXPath -> updatedDataNodesPerXPath.keySet()
290 .iterator().next() == "/bookstore/categories[@code='01']"})
291 and: 'the persistence service method is invoked for the new book (child)'
292 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName,
293 {updatedDataNodesPerXPath -> updatedDataNodesPerXPath.keySet()
294 .iterator().next() == "/bookstore/categories[@code='01']/books[@title='new']"})
295 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
296 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
299 def 'Replace data node using singular data node: #scenario.'() {
300 given: 'schema set for given anchor and dataspace references test-tree model'
301 setupSchemaSetMocks('test-tree.yang')
302 when: 'replace data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
303 objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
304 then: 'the persistence service method is invoked with correct parameters'
305 1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
306 { dataNode -> dataNode.xpath == expectedNodeXpath})
307 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
308 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
309 where: 'following parameters were used'
310 scenario | parentNodeXpath | jsonData || expectedNodeXpath
311 'top level node' | '/' | '{"test-tree": {"branch": []}}' || ['/test-tree']
312 'level 2 node' | '/test-tree' | '{"branch": [{"name":"Name"}]}' || ['/test-tree/branch[@name=\'Name\']']
313 'json list' | '/test-tree' | '{"branch": [{"name":"Name1"}, {"name":"Name2"}]}' || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"]
316 def 'Replace data node using multiple data nodes: #scenario.'() {
317 given: 'schema set for given anchor and dataspace references test-tree model'
318 setupSchemaSetMocks('test-tree.yang')
319 when: 'replace data method is invoked with a map of xpaths and json data'
320 objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, nodesJsonData, observedTimestamp)
321 then: 'the persistence service method is invoked with correct parameters'
322 1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
323 { dataNode -> dataNode.xpath == expectedNodeXpath})
324 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
325 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
326 where: 'following parameters were used'
327 scenario | nodesJsonData || expectedNodeXpath
328 'top level node' | ['/' : '{"test-tree": {"branch": []}}', '/test-tree' : '{"branch": [{"name":"Name"}]}'] || ["/test-tree", "/test-tree/branch[@name='Name']"]
329 '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"]
330 'json list' | ['/test-tree' : '{"branch": [{"name":"Name1"}, {"name":"Name2"}]}'] || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"]
333 def 'Replace data node with concurrency exception in persistence layer.'() {
334 given: 'the persistence layer throws an concurrency exception'
335 def originalException = new ConcurrencyException('message', 'details')
336 mockCpsDataPersistenceService.updateDataNodesAndDescendants(*_) >> { throw originalException }
337 setupSchemaSetMocks('test-tree.yang')
338 when: 'attempt to replace data node'
339 objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, ['/' : '{"test-tree": {}}'] , observedTimestamp)
340 then: 'the same exception is thrown up'
341 def thrownUp = thrown(ConcurrencyException)
342 assert thrownUp == originalException
345 def 'Replace list content data fragment under parent node.'() {
346 given: 'schema set for given anchor and dataspace references test-tree model'
347 setupSchemaSetMocks('test-tree.yang')
348 when: 'replace list data method is invoked with list element json data'
349 def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
350 objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
351 then: 'the persistence service method is invoked with correct parameters'
352 1 * mockCpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, '/test-tree',
353 { dataNodeCollection ->
355 assert dataNodeCollection.size() == 2
356 assert dataNodeCollection.collect { it.getXpath() }
357 .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
361 and: 'the CpsValidator is called on the dataspaceName and AnchorName twice'
362 2 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
365 def 'Replace whole list content with empty list element.'() {
366 given: 'schema set for given anchor and dataspace references test-tree model'
367 setupSchemaSetMocks('test-tree.yang')
368 when: 'replace list data method is invoked with empty list'
369 def jsonData = '{"branch": []}'
370 objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
371 then: 'invalid data exception is thrown'
372 thrown(DataValidationException)
375 def 'Delete list element under existing node.'() {
376 when: 'delete list data method is invoked with list element json data'
377 objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp)
378 then: 'the persistence service method is invoked with correct parameters'
379 1 * mockCpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, '/test-tree/branch')
380 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
381 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
384 def 'Delete multiple list elements under existing node.'() {
385 when: 'delete multiple list data method is invoked with list element json data'
386 objectUnderTest.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'], observedTimestamp)
387 then: 'the persistence service method is invoked with correct parameters'
388 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'])
389 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
390 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
393 def 'Delete data node under anchor and dataspace.'() {
394 when: 'delete data node method is invoked with correct parameters'
395 objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
396 then: 'the persistence service method is invoked with the correct parameters'
397 1 * mockCpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, '/data-node')
398 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
399 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
402 def 'Delete all data nodes for a given anchor and dataspace.'() {
403 when: 'delete data nodes method is invoked with correct parameters'
404 objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
405 then: 'the CpsValidator is called on the dataspaceName and AnchorName'
406 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
407 and: 'the persistence service method is invoked with the correct parameters'
408 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName)
411 def 'Delete all data nodes for a given anchor and dataspace with batch exception in persistence layer.'() {
412 given: 'a batch exception in persistence layer'
413 def originalException = new DataNodeNotFoundExceptionBatch('ds1','a1',[])
414 mockCpsDataPersistenceService.deleteDataNodes(*_) >> { throw originalException }
415 when: 'attempt to delete data nodes'
416 objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
417 then: 'the original exception is thrown up'
418 def thrownUp = thrown(DataNodeNotFoundExceptionBatch)
419 assert thrownUp == originalException
420 and: 'the exception details contain the expected data'
421 assert thrownUp.details.contains('ds1')
422 assert thrownUp.details.contains('a1')
425 def 'Delete all data nodes for given dataspace and multiple anchors.'() {
426 given: 'schema set for given anchors and dataspace references test tree model'
427 setupSchemaSetMocks('test-tree.yang')
428 mockCpsAnchorService.getAnchors(dataspaceName, ['anchor1', 'anchor2']) >>
429 [new Anchor(name: 'anchor1', dataspaceName: dataspaceName),
430 new Anchor(name: 'anchor2', dataspaceName: dataspaceName)]
431 when: 'delete data node method is invoked with correct parameters'
432 objectUnderTest.deleteDataNodes(dataspaceName, ['anchor1', 'anchor2'], observedTimestamp)
433 then: 'the CpsValidator is called on the dataspace name and the anchor names'
434 2 * mockCpsValidator.validateNameCharacters(_)
435 and: 'the persistence service method is invoked with the correct parameters'
436 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, _ as Collection<String>)
439 def 'Start session.'() {
440 when: 'start session method is called'
441 objectUnderTest.startSession()
442 then: 'the persistence service method to start session is invoked'
443 1 * mockCpsDataPersistenceService.startSession()
446 def 'Start session with Session Manager Exceptions.'() {
447 given: 'the persistence layer throws an Session Manager Exception'
448 mockCpsDataPersistenceService.startSession() >> { throw originalException }
449 when: 'attempt to start session'
450 objectUnderTest.startSession()
451 then: 'the original exception is thrown up'
452 def thrownUp = thrown(SessionManagerException)
453 assert thrownUp == originalException
454 where: 'variations of Session Manager Exception are used'
455 originalException << [ new SessionManagerException('message','details'),
456 new SessionManagerException('message','details', new Exception('cause')),
457 new SessionTimeoutException('message','details', new Exception('cause'))]
460 def 'Close session.'(){
461 given: 'session Id from calling the start session method'
462 def sessionId = objectUnderTest.startSession()
463 when: 'close session method is called'
464 objectUnderTest.closeSession(sessionId)
465 then: 'the persistence service method to close session is invoked'
466 1 * mockCpsDataPersistenceService.closeSession(sessionId)
469 def 'Lock anchor with no timeout parameter.'(){
470 when: 'lock anchor method with no timeout parameter with details of anchor entity to lock'
471 objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName')
472 then: 'the persistence service method to lock anchor is invoked with default timeout'
473 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 300L)
476 def 'Lock anchor with timeout parameter.'(){
477 when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock'
478 objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
479 then: 'the persistence service method to lock anchor is invoked with the given timeout'
480 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
483 def 'Exception is thrown while publishing the notification.'(){
484 given: 'schema set for given anchor and dataspace references test-tree model'
485 setupSchemaSetMocks('test-tree.yang')
486 when: 'publisher set to throw an exception'
487 mockDataUpdateEventsService.publishCpsDataUpdateEvent(_, _, _, _) >> { throw new Exception("publishing failed")}
488 and: 'an update event is performed'
489 objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/', '{"test-tree": {"branch": []}}', observedTimestamp)
490 then: 'the exception is not bubbled up'
492 and: "the exception message is logged"
493 def logs = loggingListAppender.list.toString()
494 assert logs.contains('Failed to send message to notification service')
496 def setupSchemaSetMocks(String... yangResources) {
497 def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
498 mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
499 def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
500 def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
501 mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext