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.CpsAnchorService
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
46 import java.time.OffsetDateTime
47 import java.util.stream.Collectors
49 class CpsDataServiceImplSpec extends Specification {
50 def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService)
51 def mockCpsAnchorService = Mock(CpsAnchorService)
52 def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)
53 def mockCpsValidator = Mock(CpsValidator)
54 def timedYangParser = new TimedYangParser()
55 def mockCpsDeltaService = Mock(CpsDeltaService);
57 def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAnchorService,
58 mockYangTextSchemaSourceSetCache, mockCpsValidator, timedYangParser, mockCpsDeltaService)
61 mockCpsAnchorService.getAnchor(dataspaceName, anchorName) >> anchor
62 mockCpsAnchorService.getAnchor(dataspaceName, ANCHOR_NAME_1) >> anchor1
63 mockCpsAnchorService.getAnchor(dataspaceName, ANCHOR_NAME_2) >> anchor2
67 static def ANCHOR_NAME_1 = 'some-anchor-1'
69 static def ANCHOR_NAME_2 = 'some-anchor-2'
70 def dataspaceName = 'some-dataspace'
71 def anchorName = 'some-anchor'
72 def schemaSetName = 'some-schema-set'
73 def anchor = Anchor.builder().name(anchorName).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build()
74 def anchor1 = Anchor.builder().name(ANCHOR_NAME_1).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build()
75 def anchor2 = Anchor.builder().name(ANCHOR_NAME_2).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build()
76 def observedTimestamp = OffsetDateTime.now()
78 def 'Saving #scenario data.'() {
79 given: 'schema set for given anchor and dataspace references test-tree model'
80 setupSchemaSetMocks('test-tree.yang')
81 when: 'save data method is invoked with test-tree #scenario data'
82 def data = TestUtils.getResourceFileContent(dataFile)
83 objectUnderTest.saveData(dataspaceName, anchorName, data, observedTimestamp, contentType)
84 then: 'the persistence service method is invoked with correct parameters'
85 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
86 { dataNode -> dataNode.xpath[0] == '/test-tree' })
87 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
88 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
89 where: 'given parameters'
90 scenario | dataFile | contentType
91 'json' | 'test-tree.json' | ContentType.JSON
92 'xml' | 'test-tree.xml' | ContentType.XML
95 def 'Saving data with error: #scenario.'() {
96 given: 'schema set for given anchor and dataspace references test-tree model'
97 setupSchemaSetMocks('test-tree.yang')
98 when: 'save data method is invoked with test-tree json data'
99 objectUnderTest.saveData(dataspaceName, anchorName, invalidData, observedTimestamp, contentType)
100 then: 'a data validation exception is thrown with the correct message'
101 def exceptionThrown = thrown(DataValidationException)
102 assert exceptionThrown.message.startsWith(expectedMessage)
103 where: 'given parameters'
104 scenario | invalidData | contentType || expectedMessage
105 'no data nodes' | '{}' | ContentType.JSON || 'No data nodes'
106 'invalid json' | '{invalid json' | ContentType.JSON || 'Failed to parse json data'
107 'invalid xml' | '<invalid xml' | ContentType.XML || 'Failed to parse xml data'
110 def 'Saving list element data fragment under Root node.'() {
111 given: 'schema set for given anchor and dataspace references bookstore model'
112 setupSchemaSetMocks('bookstore.yang')
113 when: 'save data method is invoked with list element json data'
114 def jsonData = '{"bookstore-address":[{"bookstore-name":"Easons","address":"Dublin,Ireland","postal-code":"D02HA21"}]}'
115 objectUnderTest.saveListElements(dataspaceName, anchorName, '/', jsonData, observedTimestamp)
116 then: 'the persistence service method is invoked with correct parameters'
117 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
118 { dataNodeCollection ->
120 assert dataNodeCollection.size() == 1
121 assert dataNodeCollection.collect { it.getXpath() }
122 .containsAll(['/bookstore-address[@bookstore-name=\'Easons\']'])
126 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
127 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
130 def 'Saving child data fragment under existing node.'() {
131 given: 'schema set for given anchor and dataspace references test-tree model'
132 setupSchemaSetMocks('test-tree.yang')
133 when: 'save data method is invoked with test-tree json data'
134 def jsonData = '{"branch": [{"name": "New"}]}'
135 objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
136 then: 'the persistence service method is invoked with correct parameters'
137 1 * mockCpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, '/test-tree',
138 { dataNode -> dataNode.xpath[0] == '/test-tree/branch[@name=\'New\']' })
139 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
140 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
143 def 'Saving list element data fragment under existing node.'() {
144 given: 'schema set for given anchor and dataspace references test-tree model'
145 setupSchemaSetMocks('test-tree.yang')
146 when: 'save data method is invoked with list element json data'
147 def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
148 objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
149 then: 'the persistence service method is invoked with correct parameters'
150 1 * mockCpsDataPersistenceService.addListElements(dataspaceName, anchorName, '/test-tree',
151 { dataNodeCollection ->
153 assert dataNodeCollection.size() == 2
154 assert dataNodeCollection.collect { it.getXpath() }
155 .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
159 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
160 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
163 def 'Saving collection of a batch with data fragment under existing node.'() {
164 given: 'schema set for given anchor and dataspace references test-tree model'
165 setupSchemaSetMocks('test-tree.yang')
166 when: 'save data method is invoked with list element json data'
167 def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
168 objectUnderTest.saveListElementsBatch(dataspaceName, anchorName, '/test-tree', [jsonData], observedTimestamp)
169 then: 'the persistence service method is invoked with correct parameters'
170 1 * mockCpsDataPersistenceService.addMultipleLists(dataspaceName, anchorName, '/test-tree',_) >> {
172 def listElementsCollection = args[3] as Collection<Collection<DataNode>>
173 assert listElementsCollection.size() == 1
174 def listOfXpaths = listElementsCollection.stream().flatMap(x -> x.stream()).map(it-> it.xpath).collect(Collectors.toList())
175 assert listOfXpaths.size() == 2
176 assert listOfXpaths.containsAll(['/test-tree/branch[@name=\'B\']','/test-tree/branch[@name=\'A\']'])
181 def 'Saving empty list element data fragment.'() {
182 given: 'schema set for given anchor and dataspace references test-tree model'
183 setupSchemaSetMocks('test-tree.yang')
184 when: 'save data method is invoked with an empty list'
185 def jsonData = '{"branch": []}'
186 objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
187 then: 'invalid data exception is thrown'
188 thrown(DataValidationException)
191 def 'Get all data nodes #scenario.'() {
192 given: 'persistence service returns data for GET request'
193 mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption) >> dataNode
194 expect: 'service returns same data if using same parameters'
195 objectUnderTest.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption) == dataNode
196 where: 'following parameters were used'
197 scenario | xpath | fetchDescendantsOption | dataNode
198 'with root node xpath and descendants' | '/' | FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath-1').build(), new DataNodeBuilder().withXpath('/xpath-2').build()]
199 'with root node xpath and no descendants' | '/' | FetchDescendantsOption.OMIT_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath-1').build(), new DataNodeBuilder().withXpath('/xpath-2').build()]
200 'with valid xpath and descendants' | '/xpath'| FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath').build()]
201 'with valid xpath and no descendants' | '/xpath'| FetchDescendantsOption.OMIT_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath').build()]
204 def 'Get all data nodes over multiple xpaths with option #fetchDescendantsOption.'() {
205 def xpath1 = '/xpath-1'
206 def xpath2 = '/xpath-2'
207 def dataNode = [new DataNodeBuilder().withXpath(xpath1).build(), new DataNodeBuilder().withXpath(xpath2).build()]
208 given: 'persistence service returns data for get data request'
209 mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, anchorName, [xpath1, xpath2], fetchDescendantsOption) >> dataNode
210 expect: 'service returns same data if uses same parameters'
211 objectUnderTest.getDataNodesForMultipleXpaths(dataspaceName, anchorName, [xpath1, xpath2], fetchDescendantsOption) == dataNode
212 where: 'all fetch options are supported'
213 fetchDescendantsOption << [FetchDescendantsOption.OMIT_DESCENDANTS, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS]
216 def 'Get delta between 2 anchors'() {
217 given: 'some xpath, source and target data nodes'
219 def sourceDataNodes = [new DataNodeBuilder().withXpath(xpath).build()]
220 def targetDataNodes = [new DataNodeBuilder().withXpath(xpath).build()]
221 when: 'attempt to get delta between 2 anchors'
222 objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
223 then: 'the dataspace and anchor names are validated'
224 2 * mockCpsValidator.validateNameCharacters(_)
225 and: 'data nodes are fetched using appropriate persistence layer method'
226 mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> sourceDataNodes
227 mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_2, [xpath], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> targetDataNodes
228 and: 'appropriate delta service method is invoked once with correct source and target data nodes'
229 1 * mockCpsDeltaService.getDeltaReports(sourceDataNodes, targetDataNodes)
232 def 'Update data node leaves: #scenario.'() {
233 given: 'schema set for given anchor and dataspace references test-tree model'
234 setupSchemaSetMocks('test-tree.yang')
235 when: 'update data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
236 objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
237 then: 'the persistence service method is invoked with correct parameters'
238 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, {dataNode -> dataNode.keySet()[0] == expectedNodeXpath})
239 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
240 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
241 where: 'following parameters were used'
242 scenario | parentNodeXpath | jsonData || expectedNodeXpath
243 'top level node' | '/' | '{"test-tree": {"branch": []}}' || '/test-tree'
244 'level 2 node' | '/test-tree' | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']'
247 def 'Update list-element data node with : #scenario.'() {
248 given: 'schema set for given anchor and dataspace references bookstore model'
249 setupSchemaSetMocks('bookstore.yang')
250 when: 'update data method is invoked with json data #jsonData and parent node xpath'
251 objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/bookstore/categories[@code=2]',
252 jsonData, observedTimestamp)
253 then: 'the persistence service method is invoked with correct parameters'
254 thrown(DataValidationException)
255 where: 'following parameters were used'
257 'multiple expectedLeaves' | '{"code": "01","name": "some-name"}'
258 'one leaf' | '{"name": "some-name"}'
261 def 'Update data nodes in different containers.' () {
262 given: 'schema set for given dataspace and anchor refers multipleDataTree model'
263 setupSchemaSetMocks('multipleDataTree.yang')
264 and: 'json string with multiple data trees'
265 def parentNodeXpath = '/'
266 def updatedJsonData = '{"first-container":{"a-leaf":"a-new-Value"},"last-container":{"x-leaf":"x-new-value"}}'
267 when: 'update operation is performed on multiple data nodes'
268 objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, updatedJsonData, observedTimestamp)
269 then: 'the persistence service method is invoked with correct parameters'
270 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, {dataNode -> dataNode.keySet()[index] == expectedNodeXpath})
271 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
272 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
273 where: 'the following parameters were used'
274 index | expectedNodeXpath
275 0 | '/first-container'
276 1 | '/last-container'
279 def 'Update Bookstore node leaves and child.' () {
280 given: 'a DMI registry model'
281 setupSchemaSetMocks('bookstore.yang')
282 and: 'json update for a category (parent) and new book (child)'
283 def jsonData = '{"categories":[{"code":01,"name":"Romance","books": [{"title": "new"}]}]}'
284 when: 'update data method is invoked with json data and parent node xpath'
285 objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName, '/bookstore', jsonData, observedTimestamp)
286 then: 'the persistence service method is invoked for the category (parent)'
287 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName,
288 {updatedDataNodesPerXPath -> updatedDataNodesPerXPath.keySet()
289 .iterator().next() == "/bookstore/categories[@code='01']"})
290 and: 'the persistence service method is invoked for the new book (child)'
291 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName,
292 {updatedDataNodesPerXPath -> updatedDataNodesPerXPath.keySet()
293 .iterator().next() == "/bookstore/categories[@code='01']/books[@title='new']"})
294 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
295 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
298 def 'Replace data node using singular data node: #scenario.'() {
299 given: 'schema set for given anchor and dataspace references test-tree model'
300 setupSchemaSetMocks('test-tree.yang')
301 when: 'replace data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
302 objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
303 then: 'the persistence service method is invoked with correct parameters'
304 1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
305 { dataNode -> dataNode.xpath == expectedNodeXpath})
306 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
307 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
308 where: 'following parameters were used'
309 scenario | parentNodeXpath | jsonData || expectedNodeXpath
310 'top level node' | '/' | '{"test-tree": {"branch": []}}' || ['/test-tree']
311 'level 2 node' | '/test-tree' | '{"branch": [{"name":"Name"}]}' || ['/test-tree/branch[@name=\'Name\']']
312 'json list' | '/test-tree' | '{"branch": [{"name":"Name1"}, {"name":"Name2"}]}' || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"]
315 def 'Replace data node using multiple data nodes: #scenario.'() {
316 given: 'schema set for given anchor and dataspace references test-tree model'
317 setupSchemaSetMocks('test-tree.yang')
318 when: 'replace data method is invoked with a map of xpaths and json data'
319 objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, nodesJsonData, observedTimestamp)
320 then: 'the persistence service method is invoked with correct parameters'
321 1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
322 { dataNode -> dataNode.xpath == expectedNodeXpath})
323 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
324 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
325 where: 'following parameters were used'
326 scenario | nodesJsonData || expectedNodeXpath
327 'top level node' | ['/' : '{"test-tree": {"branch": []}}', '/test-tree' : '{"branch": [{"name":"Name"}]}'] || ["/test-tree", "/test-tree/branch[@name='Name']"]
328 '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"]
329 'json list' | ['/test-tree' : '{"branch": [{"name":"Name1"}, {"name":"Name2"}]}'] || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"]
332 def 'Replace data node with concurrency exception in persistence layer.'() {
333 given: 'the persistence layer throws an concurrency exception'
334 def originalException = new ConcurrencyException('message', 'details')
335 mockCpsDataPersistenceService.updateDataNodesAndDescendants(*_) >> { throw originalException }
336 setupSchemaSetMocks('test-tree.yang')
337 when: 'attempt to replace data node'
338 objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, ['/' : '{"test-tree": {}}'] , observedTimestamp)
339 then: 'the same exception is thrown up'
340 def thrownUp = thrown(ConcurrencyException)
341 assert thrownUp == originalException
344 def 'Replace list content data fragment under parent node.'() {
345 given: 'schema set for given anchor and dataspace references test-tree model'
346 setupSchemaSetMocks('test-tree.yang')
347 when: 'replace list data method is invoked with list element json data'
348 def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
349 objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
350 then: 'the persistence service method is invoked with correct parameters'
351 1 * mockCpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, '/test-tree',
352 { dataNodeCollection ->
354 assert dataNodeCollection.size() == 2
355 assert dataNodeCollection.collect { it.getXpath() }
356 .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
360 and: 'the CpsValidator is called on the dataspaceName and AnchorName twice'
361 2 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
364 def 'Replace whole list content with empty list element.'() {
365 given: 'schema set for given anchor and dataspace references test-tree model'
366 setupSchemaSetMocks('test-tree.yang')
367 when: 'replace list data method is invoked with empty list'
368 def jsonData = '{"branch": []}'
369 objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
370 then: 'invalid data exception is thrown'
371 thrown(DataValidationException)
374 def 'Delete list element under existing node.'() {
375 when: 'delete list data method is invoked with list element json data'
376 objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp)
377 then: 'the persistence service method is invoked with correct parameters'
378 1 * mockCpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, '/test-tree/branch')
379 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
380 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
383 def 'Delete multiple list elements under existing node.'() {
384 when: 'delete multiple list data method is invoked with list element json data'
385 objectUnderTest.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'], observedTimestamp)
386 then: 'the persistence service method is invoked with correct parameters'
387 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'])
388 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
389 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
392 def 'Delete data node under anchor and dataspace.'() {
393 when: 'delete data node method is invoked with correct parameters'
394 objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
395 then: 'the persistence service method is invoked with the correct parameters'
396 1 * mockCpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, '/data-node')
397 and: 'the CpsValidator is called on the dataspaceName and AnchorName'
398 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
401 def 'Delete all data nodes for a given anchor and dataspace.'() {
402 when: 'delete data nodes method is invoked with correct parameters'
403 objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
404 then: 'the CpsValidator is called on the dataspaceName and AnchorName'
405 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
406 and: 'the persistence service method is invoked with the correct parameters'
407 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName)
410 def 'Delete all data nodes for a given anchor and dataspace with batch exception in persistence layer.'() {
411 given: 'a batch exception in persistence layer'
412 def originalException = new DataNodeNotFoundExceptionBatch('ds1','a1',[])
413 mockCpsDataPersistenceService.deleteDataNodes(*_) >> { throw originalException }
414 when: 'attempt to delete data nodes'
415 objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
416 then: 'the original exception is thrown up'
417 def thrownUp = thrown(DataNodeNotFoundExceptionBatch)
418 assert thrownUp == originalException
419 and: 'the exception details contain the expected data'
420 assert thrownUp.details.contains('ds1')
421 assert thrownUp.details.contains('a1')
424 def 'Delete all data nodes for given dataspace and multiple anchors.'() {
425 given: 'schema set for given anchors and dataspace references test tree model'
426 setupSchemaSetMocks('test-tree.yang')
427 mockCpsAnchorService.getAnchors(dataspaceName, ['anchor1', 'anchor2']) >>
428 [new Anchor(name: 'anchor1', dataspaceName: dataspaceName),
429 new Anchor(name: 'anchor2', dataspaceName: dataspaceName)]
430 when: 'delete data node method is invoked with correct parameters'
431 objectUnderTest.deleteDataNodes(dataspaceName, ['anchor1', 'anchor2'], observedTimestamp)
432 then: 'the CpsValidator is called on the dataspace name and the anchor names'
433 2 * mockCpsValidator.validateNameCharacters(_)
434 and: 'the persistence service method is invoked with the correct parameters'
435 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, _ as Collection<String>)
438 def 'Start session.'() {
439 when: 'start session method is called'
440 objectUnderTest.startSession()
441 then: 'the persistence service method to start session is invoked'
442 1 * mockCpsDataPersistenceService.startSession()
445 def 'Start session with Session Manager Exceptions.'() {
446 given: 'the persistence layer throws an Session Manager Exception'
447 mockCpsDataPersistenceService.startSession() >> { throw originalException }
448 when: 'attempt to start session'
449 objectUnderTest.startSession()
450 then: 'the original exception is thrown up'
451 def thrownUp = thrown(SessionManagerException)
452 assert thrownUp == originalException
453 where: 'variations of Session Manager Exception are used'
454 originalException << [ new SessionManagerException('message','details'),
455 new SessionManagerException('message','details', new Exception('cause')),
456 new SessionTimeoutException('message','details', new Exception('cause'))]
459 def 'Close session.'(){
460 given: 'session Id from calling the start session method'
461 def sessionId = objectUnderTest.startSession()
462 when: 'close session method is called'
463 objectUnderTest.closeSession(sessionId)
464 then: 'the persistence service method to close session is invoked'
465 1 * mockCpsDataPersistenceService.closeSession(sessionId)
468 def 'Lock anchor with no timeout parameter.'(){
469 when: 'lock anchor method with no timeout parameter with details of anchor entity to lock'
470 objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName')
471 then: 'the persistence service method to lock anchor is invoked with default timeout'
472 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 300L)
475 def 'Lock anchor with timeout parameter.'(){
476 when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock'
477 objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
478 then: 'the persistence service method to lock anchor is invoked with the given timeout'
479 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
482 def setupSchemaSetMocks(String... yangResources) {
483 def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
484 mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
485 def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
486 def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
487 mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext