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