Remove the dependency-cycle between beans
[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.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
48
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);
56
57     def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAnchorService,
58             mockYangTextSchemaSourceSetCache, mockCpsValidator, timedYangParser, mockCpsDeltaService)
59
60     def setup() {
61         mockCpsAnchorService.getAnchor(dataspaceName, anchorName) >> anchor
62         mockCpsAnchorService.getAnchor(dataspaceName, ANCHOR_NAME_1) >> anchor1
63         mockCpsAnchorService.getAnchor(dataspaceName, ANCHOR_NAME_2) >> anchor2
64     }
65
66     @Shared
67     static def ANCHOR_NAME_1 = 'some-anchor-1'
68     @Shared
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()
77
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
93     }
94
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'
108     }
109
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 ->
119                     {
120                         assert dataNodeCollection.size() == 1
121                         assert dataNodeCollection.collect { it.getXpath() }
122                             .containsAll(['/bookstore-address[@bookstore-name=\'Easons\']'])
123                     }
124                 }
125             )
126         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
127             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
128     }
129
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)
141     }
142
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 ->
152                     {
153                         assert dataNodeCollection.size() == 2
154                         assert dataNodeCollection.collect { it.getXpath() }
155                             .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
156                     }
157                 }
158             )
159         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
160             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
161     }
162
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',_) >> {
171                 args -> {
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\']'])
177                 }
178             }
179     }
180
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)
189     }
190
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()]
202     }
203
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]
214     }
215
216     def 'Get delta between 2 anchors'() {
217         given: 'some xpath, source and target data nodes'
218             def xpath = '/xpath'
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)
230     }
231
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\']'
245     }
246
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'
256             scenario                  | jsonData
257             'multiple expectedLeaves' | '{"code": "01","name": "some-name"}'
258             'one leaf'                | '{"name": "some-name"}'
259     }
260
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'
277     }
278
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)
296     }
297
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']"]
313     }
314
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']"]
330     }
331
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
342     }
343
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 ->
353                     {
354                         assert dataNodeCollection.size() == 2
355                         assert dataNodeCollection.collect { it.getXpath() }
356                             .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
357                     }
358                 }
359             )
360         and: 'the CpsValidator is called on the dataspaceName and AnchorName twice'
361             2 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
362     }
363
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)
372     }
373
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)
381     }
382
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)
390     }
391
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)
399     }
400
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)
408     }
409
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')
422     }
423
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>)
436     }
437
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()
443     }
444
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'))]
457     }
458
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)
466     }
467
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)
473     }
474
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)
480     }
481
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
488     }
489
490 }