CPS Delta API: Update action for delta service
[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.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 empty list element data fragment.'() {
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 an empty list'
167             def jsonData = '{"branch": []}'
168             objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
169         then: 'invalid data exception is thrown'
170             thrown(DataValidationException)
171     }
172
173     def 'Get all data nodes #scenario.'() {
174         given: 'persistence service returns data for GET request'
175             mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption) >> dataNode
176         expect: 'service returns same data if using same parameters'
177             objectUnderTest.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption) == dataNode
178         where: 'following parameters were used'
179             scenario                                   | xpath   | fetchDescendantsOption                         |   dataNode
180             'with root node xpath and descendants'     | '/'     | FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath-1').build(), new DataNodeBuilder().withXpath('/xpath-2').build()]
181             'with root node xpath and no descendants'  | '/'     | FetchDescendantsOption.OMIT_DESCENDANTS        | [new DataNodeBuilder().withXpath('/xpath-1').build(), new DataNodeBuilder().withXpath('/xpath-2').build()]
182             'with valid xpath and descendants'         | '/xpath'| FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath').build()]
183             'with valid xpath and no descendants'      | '/xpath'| FetchDescendantsOption.OMIT_DESCENDANTS        | [new DataNodeBuilder().withXpath('/xpath').build()]
184     }
185
186     def 'Get all data nodes over multiple xpaths with option #fetchDescendantsOption.'() {
187         def xpath1 = '/xpath-1'
188         def xpath2 = '/xpath-2'
189         def dataNode = [new DataNodeBuilder().withXpath(xpath1).build(), new DataNodeBuilder().withXpath(xpath2).build()]
190         given: 'persistence service returns data for get data request'
191             mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, anchorName, [xpath1, xpath2], fetchDescendantsOption) >> dataNode
192         expect: 'service returns same data if uses same parameters'
193             objectUnderTest.getDataNodesForMultipleXpaths(dataspaceName, anchorName, [xpath1, xpath2], fetchDescendantsOption) == dataNode
194         where: 'all fetch options are supported'
195             fetchDescendantsOption << [FetchDescendantsOption.OMIT_DESCENDANTS, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS]
196     }
197
198     def 'Get delta between 2 anchors'() {
199         given: 'some xpath, source and target data nodes'
200             def xpath = '/xpath'
201             def sourceDataNodes = [new DataNodeBuilder().withXpath(xpath).build()]
202             def targetDataNodes = [new DataNodeBuilder().withXpath(xpath).build()]
203         when: 'attempt to get delta between 2 anchors'
204             objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
205         then: 'the dataspace and anchor names are validated'
206             2 * mockCpsValidator.validateNameCharacters(_)
207         and: 'data nodes are fetched using appropriate persistence layer method'
208             mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> sourceDataNodes
209             mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_2, [xpath], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> targetDataNodes
210         and: 'appropriate delta service method is invoked once with correct source and target data nodes'
211             1 * mockCpsDeltaService.getDeltaReports(sourceDataNodes, targetDataNodes)
212     }
213
214     def 'Update data node leaves: #scenario.'() {
215         given: 'schema set for given anchor and dataspace references test-tree model'
216             setupSchemaSetMocks('test-tree.yang')
217         when: 'update data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
218             objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
219         then: 'the persistence service method is invoked with correct parameters'
220             1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, {dataNode -> dataNode.keySet()[0] == expectedNodeXpath})
221         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
222             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
223         where: 'following parameters were used'
224             scenario         | parentNodeXpath | jsonData                        || expectedNodeXpath
225             'top level node' | '/'             | '{"test-tree": {"branch": []}}' || '/test-tree'
226             'level 2 node'   | '/test-tree'    | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']'
227     }
228
229     def 'Update list-element data node with : #scenario.'() {
230         given: 'schema set for given anchor and dataspace references bookstore model'
231             setupSchemaSetMocks('bookstore.yang')
232         when: 'update data method is invoked with json data #jsonData and parent node xpath'
233             objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/bookstore/categories[@code=2]',
234                 jsonData, observedTimestamp)
235         then: 'the persistence service method is invoked with correct parameters'
236             thrown(DataValidationException)
237         where: 'following parameters were used'
238             scenario                  | jsonData
239             'multiple expectedLeaves' | '{"code": "01","name": "some-name"}'
240             'one leaf'                | '{"name": "some-name"}'
241     }
242
243     def 'Update data nodes in different containers.' () {
244         given: 'schema set for given dataspace and anchor refers multipleDataTree model'
245             setupSchemaSetMocks('multipleDataTree.yang')
246         and: 'json string with multiple data trees'
247             def parentNodeXpath = '/'
248             def updatedJsonData = '{"first-container":{"a-leaf":"a-new-Value"},"last-container":{"x-leaf":"x-new-value"}}'
249         when: 'update operation is performed on multiple data nodes'
250             objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, updatedJsonData, observedTimestamp)
251         then: 'the persistence service method is invoked with correct parameters'
252             1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, {dataNode -> dataNode.keySet()[index] == expectedNodeXpath})
253         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
254             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
255         where: 'the following parameters were used'
256             index | expectedNodeXpath
257             0     | '/first-container'
258             1     | '/last-container'
259     }
260
261     def 'Update Bookstore node leaves and child.' () {
262         given: 'a DMI registry model'
263             setupSchemaSetMocks('bookstore.yang')
264         and: 'json update for a category (parent) and new book (child)'
265             def jsonData = '{"categories":[{"code":01,"name":"Romance","books": [{"title": "new"}]}]}'
266         when: 'update data method is invoked with json data and parent node xpath'
267             objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName, '/bookstore', jsonData, observedTimestamp)
268         then: 'the persistence service method is invoked for the category (parent)'
269             1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName,
270                     {updatedDataNodesPerXPath -> updatedDataNodesPerXPath.keySet()
271                                                 .iterator().next() == "/bookstore/categories[@code='01']"})
272         and: 'the persistence service method is invoked for the new book (child)'
273             1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName,
274                 {updatedDataNodesPerXPath -> updatedDataNodesPerXPath.keySet()
275                     .iterator().next() == "/bookstore/categories[@code='01']/books[@title='new']"})
276         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
277             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
278     }
279
280     def 'Replace data node using singular data node: #scenario.'() {
281         given: 'schema set for given anchor and dataspace references test-tree model'
282             setupSchemaSetMocks('test-tree.yang')
283         when: 'replace data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
284             objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
285         then: 'the persistence service method is invoked with correct parameters'
286             1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
287                     { dataNode -> dataNode.xpath == expectedNodeXpath})
288         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
289             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
290         where: 'following parameters were used'
291             scenario         | parentNodeXpath | jsonData                                           || expectedNodeXpath
292             'top level node' | '/'             | '{"test-tree": {"branch": []}}'                    || ['/test-tree']
293             'level 2 node'   | '/test-tree'    | '{"branch": [{"name":"Name"}]}'                    || ['/test-tree/branch[@name=\'Name\']']
294             'json list'      | '/test-tree'    | '{"branch": [{"name":"Name1"}, {"name":"Name2"}]}' || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"]
295     }
296
297     def 'Replace data node using multiple data nodes: #scenario.'() {
298         given: 'schema set for given anchor and dataspace references test-tree model'
299             setupSchemaSetMocks('test-tree.yang')
300         when: 'replace data method is invoked with a map of xpaths and json data'
301             objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, nodesJsonData, observedTimestamp)
302         then: 'the persistence service method is invoked with correct parameters'
303             1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
304                 { dataNode -> dataNode.xpath == expectedNodeXpath})
305         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
306             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
307         where: 'following parameters were used'
308             scenario         | nodesJsonData                                                                                                        || expectedNodeXpath
309             'top level node' | ['/' : '{"test-tree": {"branch": []}}', '/test-tree' : '{"branch": [{"name":"Name"}]}']                              || ["/test-tree", "/test-tree/branch[@name='Name']"]
310             '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"]
311             'json list'      | ['/test-tree' : '{"branch": [{"name":"Name1"}, {"name":"Name2"}]}']                                                  || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"]
312     }
313
314     def 'Replace data node with concurrency exception in persistence layer.'() {
315         given: 'the persistence layer throws an concurrency exception'
316             def originalException = new ConcurrencyException('message', 'details')
317             mockCpsDataPersistenceService.updateDataNodesAndDescendants(*_) >> { throw originalException }
318             setupSchemaSetMocks('test-tree.yang')
319         when: 'attempt to replace data node'
320             objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, ['/' : '{"test-tree": {}}'] , observedTimestamp)
321         then: 'the same exception is thrown up'
322             def thrownUp = thrown(ConcurrencyException)
323             assert thrownUp == originalException
324     }
325
326     def 'Replace list content data fragment under parent node.'() {
327         given: 'schema set for given anchor and dataspace references test-tree model'
328             setupSchemaSetMocks('test-tree.yang')
329         when: 'replace list data method is invoked with list element json data'
330             def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
331             objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
332         then: 'the persistence service method is invoked with correct parameters'
333             1 * mockCpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, '/test-tree',
334                 { dataNodeCollection ->
335                     {
336                         assert dataNodeCollection.size() == 2
337                         assert dataNodeCollection.collect { it.getXpath() }
338                             .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
339                     }
340                 }
341             )
342         and: 'the CpsValidator is called on the dataspaceName and AnchorName twice'
343             2 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
344     }
345
346     def 'Replace whole list content with empty list element.'() {
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 empty list'
350             def jsonData = '{"branch": []}'
351             objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
352         then: 'invalid data exception is thrown'
353             thrown(DataValidationException)
354     }
355
356     def 'Delete list element under existing node.'() {
357         when: 'delete list data method is invoked with list element json data'
358             objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp)
359         then: 'the persistence service method is invoked with correct parameters'
360             1 * mockCpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, '/test-tree/branch')
361         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
362             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
363     }
364
365     def 'Delete multiple list elements under existing node.'() {
366         when: 'delete multiple list data method is invoked with list element json data'
367             objectUnderTest.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'], observedTimestamp)
368         then: 'the persistence service method is invoked with correct parameters'
369             1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'])
370         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
371             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
372     }
373
374     def 'Delete data node under anchor and dataspace.'() {
375         when: 'delete data node method is invoked with correct parameters'
376             objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
377         then: 'the persistence service method is invoked with the correct parameters'
378             1 * mockCpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, '/data-node')
379         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
380             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
381     }
382
383     def 'Delete all data nodes for a given anchor and dataspace.'() {
384         when: 'delete data nodes method is invoked with correct parameters'
385             objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
386         then: 'the CpsValidator is called on the dataspaceName and AnchorName'
387             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
388         and: 'the persistence service method is invoked with the correct parameters'
389             1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName)
390     }
391
392     def 'Delete all data nodes for a given anchor and dataspace with batch exception in persistence layer.'() {
393         given: 'a batch exception in persistence layer'
394             def originalException = new DataNodeNotFoundExceptionBatch('ds1','a1',[])
395             mockCpsDataPersistenceService.deleteDataNodes(*_)  >> { throw originalException }
396         when: 'attempt to delete data nodes'
397             objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
398         then: 'the original exception is thrown up'
399             def thrownUp = thrown(DataNodeNotFoundExceptionBatch)
400             assert thrownUp == originalException
401         and: 'the exception details contain the expected data'
402             assert thrownUp.details.contains('ds1')
403             assert thrownUp.details.contains('a1')
404     }
405
406     def 'Delete all data nodes for given dataspace and multiple anchors.'() {
407         given: 'schema set for given anchors and dataspace references test tree model'
408             setupSchemaSetMocks('test-tree.yang')
409             mockCpsAnchorService.getAnchors(dataspaceName, ['anchor1', 'anchor2']) >>
410                 [new Anchor(name: 'anchor1', dataspaceName: dataspaceName),
411                  new Anchor(name: 'anchor2', dataspaceName: dataspaceName)]
412         when: 'delete data node method is invoked with correct parameters'
413             objectUnderTest.deleteDataNodes(dataspaceName, ['anchor1', 'anchor2'], observedTimestamp)
414         then: 'the CpsValidator is called on the dataspace name and the anchor names'
415             2 * mockCpsValidator.validateNameCharacters(_)
416         and: 'the persistence service method is invoked with the correct parameters'
417             1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, _ as Collection<String>)
418     }
419
420     def 'Start session.'() {
421         when: 'start session method is called'
422             objectUnderTest.startSession()
423         then: 'the persistence service method to start session is invoked'
424             1 * mockCpsDataPersistenceService.startSession()
425     }
426
427     def 'Start session with Session Manager Exceptions.'() {
428         given: 'the persistence layer throws an Session Manager Exception'
429             mockCpsDataPersistenceService.startSession() >> { throw originalException }
430         when: 'attempt to start session'
431             objectUnderTest.startSession()
432         then: 'the original exception is thrown up'
433             def thrownUp = thrown(SessionManagerException)
434             assert thrownUp == originalException
435         where: 'variations of Session Manager Exception are used'
436             originalException << [ new SessionManagerException('message','details'),
437                                    new SessionManagerException('message','details', new Exception('cause')),
438                                    new SessionTimeoutException('message','details', new Exception('cause'))]
439     }
440
441     def 'Close session.'(){
442         given: 'session Id from calling the start session method'
443             def sessionId = objectUnderTest.startSession()
444         when: 'close session method is called'
445             objectUnderTest.closeSession(sessionId)
446         then: 'the persistence service method to close session is invoked'
447             1 * mockCpsDataPersistenceService.closeSession(sessionId)
448     }
449
450     def 'Lock anchor with no timeout parameter.'(){
451         when: 'lock anchor method with no timeout parameter with details of anchor entity to lock'
452             objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName')
453         then: 'the persistence service method to lock anchor is invoked with default timeout'
454             1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 300L)
455     }
456
457     def 'Lock anchor with timeout parameter.'(){
458         when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock'
459             objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
460         then: 'the persistence service method to lock anchor is invoked with the given timeout'
461             1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
462     }
463
464     def setupSchemaSetMocks(String... yangResources) {
465         def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
466         mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
467         def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
468         def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
469         mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
470     }
471
472 }