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.api.CpsDeltaService
29 import org.onap.cps.spi.CpsDataPersistenceService
30 import org.onap.cps.spi.FetchDescendantsOption
31 import org.onap.cps.spi.exceptions.ConcurrencyException
32 import org.onap.cps.spi.exceptions.DataNodeNotFoundExceptionBatch
33 import org.onap.cps.spi.exceptions.DataValidationException
34 import org.onap.cps.spi.exceptions.SessionManagerException
35 import org.onap.cps.spi.exceptions.SessionTimeoutException
36 import org.onap.cps.spi.model.Anchor
37 import org.onap.cps.spi.model.DataNode
38 import org.onap.cps.spi.model.DataNodeBuilder
39 import org.onap.cps.spi.utils.CpsValidator
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.Shared
45 import spock.lang.Specification
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 mockCpsValidator = Mock(CpsValidator)
55 def timedYangParser = new TimedYangParser()
56 def mockCpsDeltaService = Mock(CpsDeltaService);
58 def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAdminService,
59 mockYangTextSchemaSourceSetCache, mockCpsValidator, timedYangParser, mockCpsDeltaService)
63 mockCpsAdminService.getAnchor(dataspaceName, anchorName) >> anchor
64 mockCpsAdminService.getAnchor(dataspaceName, ANCHOR_NAME_1) >> anchor1
65 mockCpsAdminService.getAnchor(dataspaceName, ANCHOR_NAME_2) >> anchor2
69 static def ANCHOR_NAME_1 = 'some-anchor-1'
71 static def ANCHOR_NAME_2 = 'some-anchor-2'
72 def dataspaceName = 'some-dataspace'
73 def anchorName = 'some-anchor'
74 def schemaSetName = 'some-schema-set'
75 def anchor = Anchor.builder().name(anchorName).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build()
76 def anchor1 = Anchor.builder().name(ANCHOR_NAME_1).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build()
77 def anchor2 = Anchor.builder().name(ANCHOR_NAME_2).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build()
78 def observedTimestamp = OffsetDateTime.now()
80 def 'Saving #scenario data.'() {
81 given: 'schema set for given anchor and dataspace references test-tree model'
82 setupSchemaSetMocks('test-tree.yang')
83 when: 'save data method is invoked with test-tree #scenario data'
84 def data = TestUtils.getResourceFileContent(dataFile)
85 objectUnderTest.saveData(dataspaceName, anchorName, data, observedTimestamp, contentType)
86 then: 'the persistence service method is invoked with correct parameters'
87 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
88 { dataNode -> dataNode.xpath[0] == '/test-tree' })
89 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
90 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
91 where: 'given parameters'
92 scenario | dataFile | contentType
93 'json' | 'test-tree.json' | ContentType.JSON
94 'xml' | 'test-tree.xml' | ContentType.XML
97 def 'Saving data with error: #scenario.'() {
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 json data'
101 objectUnderTest.saveData(dataspaceName, anchorName, invalidData, observedTimestamp, contentType)
102 then: 'a data validation exception is thrown with the correct message'
103 def exceptionThrown = thrown(DataValidationException)
104 assert exceptionThrown.message.startsWith(expectedMessage)
105 where: 'given parameters'
106 scenario | invalidData | contentType || expectedMessage
107 'no data nodes' | '{}' | ContentType.JSON || 'No data nodes'
108 'invalid json' | '{invalid json' | ContentType.JSON || 'Failed to parse json data'
109 'invalid xml' | '<invalid xml' | ContentType.XML || 'Failed to parse xml data'
112 def 'Saving list element data fragment under Root node.'() {
113 given: 'schema set for given anchor and dataspace references bookstore model'
114 setupSchemaSetMocks('bookstore.yang')
115 when: 'save data method is invoked with list element json data'
116 def jsonData = '{"bookstore-address":[{"bookstore-name":"Easons","address":"Dublin,Ireland","postal-code":"D02HA21"}]}'
117 objectUnderTest.saveListElements(dataspaceName, anchorName, '/', jsonData, observedTimestamp)
118 then: 'the persistence service method is invoked with correct parameters'
119 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
120 { dataNodeCollection ->
122 assert dataNodeCollection.size() == 1
123 assert dataNodeCollection.collect { it.getXpath() }
124 .containsAll(['/bookstore-address[@bookstore-name=\'Easons\']'])
128 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
129 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
132 def 'Saving child data fragment under existing node.'() {
133 given: 'schema set for given anchor and dataspace references test-tree model'
134 setupSchemaSetMocks('test-tree.yang')
135 when: 'save data method is invoked with test-tree json data'
136 def jsonData = '{"branch": [{"name": "New"}]}'
137 objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
138 then: 'the persistence service method is invoked with correct parameters'
139 1 * mockCpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, '/test-tree',
140 { dataNode -> dataNode.xpath[0] == '/test-tree/branch[@name=\'New\']' })
141 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
142 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
145 def 'Saving list element data fragment under existing node.'() {
146 given: 'schema set for given anchor and dataspace references test-tree model'
147 setupSchemaSetMocks('test-tree.yang')
148 when: 'save data method is invoked with list element json data'
149 def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
150 objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
151 then: 'the persistence service method is invoked with correct parameters'
152 1 * mockCpsDataPersistenceService.addListElements(dataspaceName, anchorName, '/test-tree',
153 { dataNodeCollection ->
155 assert dataNodeCollection.size() == 2
156 assert dataNodeCollection.collect { it.getXpath() }
157 .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
161 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
162 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
165 def 'Saving collection of a batch with data fragment under existing node.'() {
166 given: 'schema set for given anchor and dataspace references test-tree model'
167 setupSchemaSetMocks('test-tree.yang')
168 when: 'save data method is invoked with list element json data'
169 def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
170 objectUnderTest.saveListElementsBatch(dataspaceName, anchorName, '/test-tree', [jsonData], observedTimestamp)
171 then: 'the persistence service method is invoked with correct parameters'
172 1 * mockCpsDataPersistenceService.addMultipleLists(dataspaceName, anchorName, '/test-tree',_) >> {
174 def listElementsCollection = args[3] as Collection<Collection<DataNode>>
175 assert listElementsCollection.size() == 1
176 def listOfXpaths = listElementsCollection.stream().flatMap(x -> x.stream()).map(it-> it.xpath).collect(Collectors.toList())
177 assert listOfXpaths.size() == 2
178 assert listOfXpaths.containsAll(['/test-tree/branch[@name=\'B\']','/test-tree/branch[@name=\'A\']'])
183 def 'Saving empty list element data fragment.'() {
184 given: 'schema set for given anchor and dataspace references test-tree model'
185 setupSchemaSetMocks('test-tree.yang')
186 when: 'save data method is invoked with an empty list'
187 def jsonData = '{"branch": []}'
188 objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
189 then: 'invalid data exception is thrown'
190 thrown(DataValidationException)
193 def 'Get all data nodes #scenario.'() {
194 given: 'persistence service returns data for GET request'
195 mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption) >> dataNode
196 expect: 'service returns same data if using same parameters'
197 objectUnderTest.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption) == dataNode
198 where: 'following parameters were used'
199 scenario | xpath | fetchDescendantsOption | dataNode
200 'with root node xpath and descendants' | '/' | FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath-1').build(), new DataNodeBuilder().withXpath('/xpath-2').build()]
201 'with root node xpath and no descendants' | '/' | FetchDescendantsOption.OMIT_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath-1').build(), new DataNodeBuilder().withXpath('/xpath-2').build()]
202 'with valid xpath and descendants' | '/xpath'| FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath').build()]
203 'with valid xpath and no descendants' | '/xpath'| FetchDescendantsOption.OMIT_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath').build()]
206 def 'Get all data nodes over multiple xpaths with option #fetchDescendantsOption.'() {
207 def xpath1 = '/xpath-1'
208 def xpath2 = '/xpath-2'
209 def dataNode = [new DataNodeBuilder().withXpath(xpath1).build(), new DataNodeBuilder().withXpath(xpath2).build()]
210 given: 'persistence service returns data for get data request'
211 mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, anchorName, [xpath1, xpath2], fetchDescendantsOption) >> dataNode
212 expect: 'service returns same data if uses same parameters'
213 objectUnderTest.getDataNodesForMultipleXpaths(dataspaceName, anchorName, [xpath1, xpath2], fetchDescendantsOption) == dataNode
214 where: 'all fetch options are supported'
215 fetchDescendantsOption << [FetchDescendantsOption.OMIT_DESCENDANTS, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS]
218 def 'Get delta between 2 anchors'() {
219 given: 'some xpath, source and target data nodes'
221 def sourceDataNodes = [new DataNodeBuilder().withXpath(xpath).build()]
222 def targetDataNodes = [new DataNodeBuilder().withXpath(xpath).build()]
223 when: 'attempt to get delta between 2 anchors'
224 objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
225 then: 'the dataspace and anchor names are validated'
226 2 * mockCpsValidator.validateNameCharacters(_)
227 and: 'data nodes are fetched using appropriate persistence layer method'
228 mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> sourceDataNodes
229 mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_2, [xpath], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> targetDataNodes
230 and: 'appropriate delta service method is invoked once with correct source and target data nodes'
231 1 * mockCpsDeltaService.getDeltaReports(sourceDataNodes, targetDataNodes)
234 def 'Update data node leaves: #scenario.'() {
235 given: 'schema set for given anchor and dataspace references test-tree model'
236 setupSchemaSetMocks('test-tree.yang')
237 when: 'update data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
238 objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
239 then: 'the persistence service method is invoked with correct parameters'
240 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, {dataNode -> dataNode.keySet()[0] == expectedNodeXpath})
241 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
242 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
243 where: 'following parameters were used'
244 scenario | parentNodeXpath | jsonData || expectedNodeXpath
245 'top level node' | '/' | '{"test-tree": {"branch": []}}' || '/test-tree'
246 'level 2 node' | '/test-tree' | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']'
249 def 'Update list-element data node with : #scenario.'() {
250 given: 'schema set for given anchor and dataspace references bookstore model'
251 setupSchemaSetMocks('bookstore.yang')
252 when: 'update data method is invoked with json data #jsonData and parent node xpath'
253 objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/bookstore/categories[@code=2]',
254 jsonData, observedTimestamp)
255 then: 'the persistence service method is invoked with correct parameters'
256 thrown(DataValidationException)
257 where: 'following parameters were used'
259 'multiple expectedLeaves' | '{"code": "01","name": "some-name"}'
260 'one leaf' | '{"name": "some-name"}'
263 def 'Update data nodes in different containers.' () {
264 given: 'schema set for given dataspace and anchor refers multipleDataTree model'
265 setupSchemaSetMocks('multipleDataTree.yang')
266 and: 'json string with multiple data trees'
267 def parentNodeXpath = '/'
268 def updatedJsonData = '{"first-container":{"a-leaf":"a-new-Value"},"last-container":{"x-leaf":"x-new-value"}}'
269 when: 'update operation is performed on multiple data nodes'
270 objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, updatedJsonData, observedTimestamp)
271 then: 'the persistence service method is invoked with correct parameters'
272 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, {dataNode -> dataNode.keySet()[index] == expectedNodeXpath})
273 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
274 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
275 where: 'the following parameters were used'
276 index | expectedNodeXpath
277 0 | '/first-container'
278 1 | '/last-container'
281 def 'Update Bookstore node leaves and child.' () {
282 given: 'a DMI registry model'
283 setupSchemaSetMocks('bookstore.yang')
284 and: 'json update for a category (parent) and new book (child)'
285 def jsonData = '{"categories":[{"code":01,"name":"Romance","books": [{"title": "new"}]}]}'
286 when: 'update data method is invoked with json data and parent node xpath'
287 objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName, '/bookstore', jsonData, observedTimestamp)
288 then: 'the persistence service method is invoked for the category (parent)'
289 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName,
290 {updatedDataNodesPerXPath -> updatedDataNodesPerXPath.keySet()
291 .iterator().next() == "/bookstore/categories[@code='01']"})
292 and: 'the persistence service method is invoked for the new book (child)'
293 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName,
294 {updatedDataNodesPerXPath -> updatedDataNodesPerXPath.keySet()
295 .iterator().next() == "/bookstore/categories[@code='01']/books[@title='new']"})
296 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
297 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
300 def 'Replace data node using singular data node: #scenario.'() {
301 given: 'schema set for given anchor and dataspace references test-tree model'
302 setupSchemaSetMocks('test-tree.yang')
303 when: 'replace data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
304 objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
305 then: 'the persistence service method is invoked with correct parameters'
306 1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
307 { dataNode -> dataNode.xpath == expectedNodeXpath})
308 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
309 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
310 where: 'following parameters were used'
311 scenario | parentNodeXpath | jsonData || expectedNodeXpath
312 'top level node' | '/' | '{"test-tree": {"branch": []}}' || ['/test-tree']
313 'level 2 node' | '/test-tree' | '{"branch": [{"name":"Name"}]}' || ['/test-tree/branch[@name=\'Name\']']
314 'json list' | '/test-tree' | '{"branch": [{"name":"Name1"}, {"name":"Name2"}]}' || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"]
317 def 'Replace data node using multiple data nodes: #scenario.'() {
318 given: 'schema set for given anchor and dataspace references test-tree model'
319 setupSchemaSetMocks('test-tree.yang')
320 when: 'replace data method is invoked with a map of xpaths and json data'
321 objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, nodesJsonData, observedTimestamp)
322 then: 'the persistence service method is invoked with correct parameters'
323 1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
324 { dataNode -> dataNode.xpath == expectedNodeXpath})
325 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
326 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
327 where: 'following parameters were used'
328 scenario | nodesJsonData || expectedNodeXpath
329 'top level node' | ['/' : '{"test-tree": {"branch": []}}', '/test-tree' : '{"branch": [{"name":"Name"}]}'] || ["/test-tree", "/test-tree/branch[@name='Name']"]
330 '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"]
331 'json list' | ['/test-tree' : '{"branch": [{"name":"Name1"}, {"name":"Name2"}]}'] || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"]
334 def 'Replace data node with concurrency exception in persistence layer.'() {
335 given: 'the persistence layer throws an concurrency exception'
336 def originalException = new ConcurrencyException('message', 'details')
337 mockCpsDataPersistenceService.updateDataNodesAndDescendants(*_) >> { throw originalException }
338 setupSchemaSetMocks('test-tree.yang')
339 when: 'attempt to replace data node'
340 objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, ['/' : '{"test-tree": {}}'] , observedTimestamp)
341 then: 'the same exception is thrown up'
342 def thrownUp = thrown(ConcurrencyException)
343 assert thrownUp == originalException
346 def 'Replace list content data fragment under parent node.'() {
347 given: 'schema set for given anchor and dataspace references test-tree model'
348 setupSchemaSetMocks('test-tree.yang')
349 when: 'replace list data method is invoked with list element json data'
350 def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
351 objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
352 then: 'the persistence service method is invoked with correct parameters'
353 1 * mockCpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, '/test-tree',
354 { dataNodeCollection ->
356 assert dataNodeCollection.size() == 2
357 assert dataNodeCollection.collect { it.getXpath() }
358 .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
362 and: 'the CpsValidator is called on the dataspaceName and AnchorName twice'
363 2 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
366 def 'Replace whole list content with empty list element.'() {
367 given: 'schema set for given anchor and dataspace references test-tree model'
368 setupSchemaSetMocks('test-tree.yang')
369 when: 'replace list data method is invoked with empty list'
370 def jsonData = '{"branch": []}'
371 objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
372 then: 'invalid data exception is thrown'
373 thrown(DataValidationException)
376 def 'Delete list element under existing node.'() {
377 when: 'delete list data method is invoked with list element json data'
378 objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp)
379 then: 'the persistence service method is invoked with correct parameters'
380 1 * mockCpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, '/test-tree/branch')
381 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
382 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
385 def 'Delete multiple list elements under existing node.'() {
386 when: 'delete multiple list data method is invoked with list element json data'
387 objectUnderTest.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'], observedTimestamp)
388 then: 'the persistence service method is invoked with correct parameters'
389 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'])
390 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
391 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
394 def 'Delete data node under anchor and dataspace.'() {
395 when: 'delete data node method is invoked with correct parameters'
396 objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
397 then: 'the persistence service method is invoked with the correct parameters'
398 1 * mockCpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, '/data-node')
399 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
400 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
403 def 'Delete all data nodes for a given anchor and dataspace.'() {
404 when: 'delete data nodes method is invoked with correct parameters'
405 objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
406 then: 'the CpsValidator is called on the dataspaceName and AnchorName'
407 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
408 and: 'the persistence service method is invoked with the correct parameters'
409 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName)
412 def 'Delete all data nodes for a given anchor and dataspace with batch exception in persistence layer.'() {
413 given: 'a batch exception in persistence layer'
414 def originalException = new DataNodeNotFoundExceptionBatch('ds1','a1',[])
415 mockCpsDataPersistenceService.deleteDataNodes(*_) >> { throw originalException }
416 when: 'attempt to delete data nodes'
417 objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
418 then: 'the original exception is thrown up'
419 def thrownUp = thrown(DataNodeNotFoundExceptionBatch)
420 assert thrownUp == originalException
421 and: 'the exception details contain the expected data'
422 assert thrownUp.details.contains('ds1')
423 assert thrownUp.details.contains('a1')
426 def 'Delete all data nodes for given dataspace and multiple anchors.'() {
427 given: 'schema set for given anchors and dataspace references test tree model'
428 setupSchemaSetMocks('test-tree.yang')
429 mockCpsAdminService.getAnchors(dataspaceName, ['anchor1', 'anchor2']) >>
430 [new Anchor(name: 'anchor1', dataspaceName: dataspaceName),
431 new Anchor(name: 'anchor2', dataspaceName: dataspaceName)]
432 when: 'delete data node method is invoked with correct parameters'
433 objectUnderTest.deleteDataNodes(dataspaceName, ['anchor1', 'anchor2'], observedTimestamp)
434 then: 'the CpsValidator is called on the dataspace name and the anchor names'
435 2 * mockCpsValidator.validateNameCharacters(_)
436 and: 'the persistence service method is invoked with the correct parameters'
437 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, _ as Collection<String>)
440 def 'Start session.'() {
441 when: 'start session method is called'
442 objectUnderTest.startSession()
443 then: 'the persistence service method to start session is invoked'
444 1 * mockCpsDataPersistenceService.startSession()
447 def 'Start session with Session Manager Exceptions.'() {
448 given: 'the persistence layer throws an Session Manager Exception'
449 mockCpsDataPersistenceService.startSession() >> { throw originalException }
450 when: 'attempt to start session'
451 objectUnderTest.startSession()
452 then: 'the original exception is thrown up'
453 def thrownUp = thrown(SessionManagerException)
454 assert thrownUp == originalException
455 where: 'variations of Session Manager Exception are used'
456 originalException << [ new SessionManagerException('message','details'),
457 new SessionManagerException('message','details', new Exception('cause')),
458 new SessionTimeoutException('message','details', new Exception('cause'))]
461 def 'Close session.'(){
462 given: 'session Id from calling the start session method'
463 def sessionId = objectUnderTest.startSession()
464 when: 'close session method is called'
465 objectUnderTest.closeSession(sessionId)
466 then: 'the persistence service method to close session is invoked'
467 1 * mockCpsDataPersistenceService.closeSession(sessionId)
470 def 'Lock anchor with no timeout parameter.'(){
471 when: 'lock anchor method with no timeout parameter with details of anchor entity to lock'
472 objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName')
473 then: 'the persistence service method to lock anchor is invoked with default timeout'
474 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 300L)
477 def 'Lock anchor with timeout parameter.'(){
478 when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock'
479 objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
480 then: 'the persistence service method to lock anchor is invoked with the given timeout'
481 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
484 def setupSchemaSetMocks(String... yangResources) {
485 def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
486 mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
487 def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
488 def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
489 mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext