6ff708a6ea764217b396155888eb1347a087410a
[cps.git] / cps-service / src / test / groovy / org / onap / cps / api / impl / CpsDataServiceImplSpec.groovy
1 /*
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
11  *
12  *        http://www.apache.org/licenses/LICENSE-2.0
13  *
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.
19  *
20  *  SPDX-License-Identifier: Apache-2.0
21  *  ============LICENSE_END=========================================================
22  */
23
24 package org.onap.cps.api.impl
25
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
46
47 import java.time.OffsetDateTime
48 import java.util.stream.Collectors
49
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);
57
58     def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAdminService,
59             mockYangTextSchemaSourceSetCache, mockCpsValidator, timedYangParser, mockCpsDeltaService)
60
61     def setup() {
62
63         mockCpsAdminService.getAnchor(dataspaceName, anchorName) >> anchor
64         mockCpsAdminService.getAnchor(dataspaceName, ANCHOR_NAME_1) >> anchor1
65         mockCpsAdminService.getAnchor(dataspaceName, ANCHOR_NAME_2) >> anchor2
66     }
67
68     @Shared
69     static def ANCHOR_NAME_1 = 'some-anchor-1'
70     @Shared
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()
79
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
95     }
96
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'
110     }
111
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 ->
121                     {
122                         assert dataNodeCollection.size() == 1
123                         assert dataNodeCollection.collect { it.getXpath() }
124                             .containsAll(['/bookstore-address[@bookstore-name=\'Easons\']'])
125                     }
126                 }
127             )
128         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
129             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
130     }
131
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)
143     }
144
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 ->
154                     {
155                         assert dataNodeCollection.size() == 2
156                         assert dataNodeCollection.collect { it.getXpath() }
157                             .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
158                     }
159                 }
160             )
161         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
162             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
163     }
164
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',_) >> {
173                 args -> {
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\']'])
179                 }
180             }
181     }
182
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)
191     }
192
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()]
204     }
205
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]
216     }
217
218     def 'Get delta between 2 anchors'() {
219         given: 'some xpath, source and target data nodes'
220             def xpath = '/xpath'
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)
232     }
233
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\']'
247     }
248
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'
258             scenario                  | jsonData
259             'multiple expectedLeaves' | '{"code": "01","name": "some-name"}'
260             'one leaf'                | '{"name": "some-name"}'
261     }
262
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'
279     }
280
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)
298     }
299
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']"]
315     }
316
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']"]
332     }
333
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
344     }
345
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 ->
355                     {
356                         assert dataNodeCollection.size() == 2
357                         assert dataNodeCollection.collect { it.getXpath() }
358                             .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
359                     }
360                 }
361             )
362         and: 'the CpsValidator is called on the dataspaceName and AnchorName twice'
363             2 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
364     }
365
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)
374     }
375
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)
383     }
384
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)
392     }
393
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)
401     }
402
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)
410     }
411
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')
424     }
425
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>)
438     }
439
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()
445     }
446
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'))]
459     }
460
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)
468     }
469
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)
475     }
476
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)
482     }
483
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
490     }
491
492 }