Merge "Conflicting Error code"
[cps.git] / cps-service / src / test / groovy / org / onap / cps / api / impl / CpsDataServiceImplSpec.groovy
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2021-2023 Nordix Foundation
4  *  Modifications Copyright (C) 2021 Pantheon.tech
5  *  Modifications Copyright (C) 2021-2022 Bell Canada.
6  *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
7  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
8  *  Licensed under the Apache License, Version 2.0 (the "License");
9  *  you may not use this file except in compliance with the License.
10  *  You may obtain a copy of the License at
11  *
12  *        http://www.apache.org/licenses/LICENSE-2.0
13  *
14  *  Unless required by applicable law or agreed to in writing, software
15  *  distributed under the License is distributed on an "AS IS" BASIS,
16  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  *  See the License for the specific language governing permissions and
18  *  limitations under the License.
19  *
20  *  SPDX-License-Identifier: Apache-2.0
21  *  ============LICENSE_END=========================================================
22  */
23
24 package org.onap.cps.api.impl
25
26 import org.onap.cps.TestUtils
27 import org.onap.cps.api.CpsAdminService
28 import org.onap.cps.notification.NotificationService
29 import org.onap.cps.notification.Operation
30 import org.onap.cps.spi.CpsDataPersistenceService
31 import org.onap.cps.spi.FetchDescendantsOption
32 import org.onap.cps.spi.exceptions.ConcurrencyException
33 import org.onap.cps.spi.exceptions.DataNodeNotFoundExceptionBatch
34 import org.onap.cps.spi.exceptions.DataValidationException
35 import org.onap.cps.spi.exceptions.SessionManagerException
36 import org.onap.cps.spi.exceptions.SessionTimeoutException
37 import org.onap.cps.spi.model.Anchor
38 import org.onap.cps.spi.model.DataNode
39 import org.onap.cps.spi.model.DataNodeBuilder
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.Specification
45 import org.onap.cps.spi.utils.CpsValidator
46
47 import java.time.OffsetDateTime
48 import java.util.stream.Collectors
49
50 class CpsDataServiceImplSpec extends Specification {
51     def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService)
52     def mockCpsAdminService = Mock(CpsAdminService)
53     def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)
54     def mockNotificationService = Mock(NotificationService)
55     def mockCpsValidator = Mock(CpsValidator)
56     def timedYangParser = new TimedYangParser()
57
58     def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAdminService,
59             mockYangTextSchemaSourceSetCache, mockNotificationService, mockCpsValidator, timedYangParser)
60
61     def setup() {
62         mockCpsAdminService.getAnchor(dataspaceName, anchorName) >> anchor
63     }
64
65     def dataspaceName = 'some-dataspace'
66     def anchorName = 'some-anchor'
67     def schemaSetName = 'some-schema-set'
68     def anchor = Anchor.builder().name(anchorName).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build()
69     def observedTimestamp = OffsetDateTime.now()
70
71     def 'Saving #scenario data.'() {
72         given: 'schema set for given anchor and dataspace references test-tree model'
73             setupSchemaSetMocks('test-tree.yang')
74         when: 'save data method is invoked with test-tree #scenario data'
75             def data = TestUtils.getResourceFileContent(dataFile)
76             objectUnderTest.saveData(dataspaceName, anchorName, data, observedTimestamp, contentType)
77         then: 'the persistence service method is invoked with correct parameters'
78             1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
79                     { dataNode -> dataNode.xpath[0] == '/test-tree' })
80         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
81             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
82         and: 'data updated event is sent to notification service'
83             1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.CREATE, observedTimestamp)
84         where: 'given parameters'
85             scenario | dataFile         | contentType
86             'json'   | 'test-tree.json' | ContentType.JSON
87             'xml'    | 'test-tree.xml'  | ContentType.XML
88     }
89
90     def 'Saving data with error: #scenario.'() {
91         given: 'schema set for given anchor and dataspace references test-tree model'
92             setupSchemaSetMocks('test-tree.yang')
93         when: 'save data method is invoked with test-tree json data'
94             objectUnderTest.saveData(dataspaceName, anchorName, invalidData, observedTimestamp, contentType)
95         then: 'a data validation exception is thrown with the correct message'
96             def exceptionThrown  = thrown(DataValidationException)
97             assert exceptionThrown.message.startsWith(expectedMessage)
98         where: 'given parameters'
99             scenario        | invalidData     | contentType      || expectedMessage
100             'no data nodes' | '{}'            | ContentType.JSON || 'No data nodes'
101             'invalid json'  | '{invalid json' | ContentType.JSON || 'Failed to parse json data'
102             'invalid xml'   | '<invalid xml'  | ContentType.XML  || 'Failed to parse xml data'
103     }
104
105     def 'Saving #scenarioDesired data exception during notification.'() {
106         given: 'schema set for given anchor and dataspace references test-tree model'
107             setupSchemaSetMocks('test-tree.yang')
108         and: 'the notification service throws an exception'
109             mockNotificationService.processDataUpdatedEvent(*_) >> { throw new RuntimeException('to be ignored')}
110         when: 'save data method is invoked with test-tree json data'
111             def data = TestUtils.getResourceFileContent('test-tree.json')
112             objectUnderTest.saveData(dataspaceName, anchorName, data, observedTimestamp)
113         then: 'the exception is ignored'
114             noExceptionThrown()
115     }
116
117     def 'Saving list element data fragment under Root node.'() {
118         given: 'schema set for given anchor and dataspace references bookstore model'
119             setupSchemaSetMocks('bookstore.yang')
120         when: 'save data method is invoked with list element json data'
121             def jsonData = '{"bookstore-address":[{"bookstore-name":"Easons","address":"Dublin,Ireland","postal-code":"D02HA21"}]}'
122             objectUnderTest.saveListElements(dataspaceName, anchorName, '/', jsonData, observedTimestamp)
123         then: 'the persistence service method is invoked with correct parameters'
124             1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
125                 { dataNodeCollection ->
126                     {
127                         assert dataNodeCollection.size() == 1
128                         assert dataNodeCollection.collect { it.getXpath() }
129                             .containsAll(['/bookstore-address[@bookstore-name=\'Easons\']'])
130                     }
131                 }
132             )
133         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
134             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
135         and: 'data updated event is sent to notification service'
136             1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.UPDATE, observedTimestamp)
137     }
138
139     def 'Saving child data fragment under existing node.'() {
140         given: 'schema set for given anchor and dataspace references test-tree model'
141             setupSchemaSetMocks('test-tree.yang')
142         when: 'save data method is invoked with test-tree json data'
143             def jsonData = '{"branch": [{"name": "New"}]}'
144             objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
145         then: 'the persistence service method is invoked with correct parameters'
146             1 * mockCpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, '/test-tree',
147                 { dataNode -> dataNode.xpath[0] == '/test-tree/branch[@name=\'New\']' })
148         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
149             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
150         and: 'data updated event is sent to notification service'
151             1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree', Operation.CREATE, observedTimestamp)
152     }
153
154     def 'Saving list element data fragment under existing node.'() {
155         given: 'schema set for given anchor and dataspace references test-tree model'
156             setupSchemaSetMocks('test-tree.yang')
157         when: 'save data method is invoked with list element json data'
158             def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
159             objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
160         then: 'the persistence service method is invoked with correct parameters'
161             1 * mockCpsDataPersistenceService.addListElements(dataspaceName, anchorName, '/test-tree',
162                 { dataNodeCollection ->
163                     {
164                         assert dataNodeCollection.size() == 2
165                         assert dataNodeCollection.collect { it.getXpath() }
166                             .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
167                     }
168                 }
169             )
170         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
171             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
172         and: 'data updated event is sent to notification service'
173             1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree', Operation.UPDATE, observedTimestamp)
174     }
175
176     def 'Saving collection of a batch with data fragment under existing node.'() {
177         given: 'schema set for given anchor and dataspace references test-tree model'
178             setupSchemaSetMocks('test-tree.yang')
179         when: 'save data method is invoked with list element json data'
180             def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
181             objectUnderTest.saveListElementsBatch(dataspaceName, anchorName, '/test-tree', [jsonData], observedTimestamp)
182         then: 'the persistence service method is invoked with correct parameters'
183             1 * mockCpsDataPersistenceService.addMultipleLists(dataspaceName, anchorName, '/test-tree',_) >> {
184                 args -> {
185                     def listElementsCollection = args[3] as Collection<Collection<DataNode>>
186                     assert listElementsCollection.size() == 1
187                     def listOfXpaths = listElementsCollection.stream().flatMap(x -> x.stream()).map(it-> it.xpath).collect(Collectors.toList())
188                     assert listOfXpaths.size() == 2
189                     assert listOfXpaths.containsAll(['/test-tree/branch[@name=\'B\']','/test-tree/branch[@name=\'A\']'])
190                 }
191             }
192         and: 'data updated event is sent to notification service'
193             1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree', Operation.UPDATE, observedTimestamp)
194     }
195
196     def 'Saving empty list element data fragment.'() {
197         given: 'schema set for given anchor and dataspace references test-tree model'
198             setupSchemaSetMocks('test-tree.yang')
199         when: 'save data method is invoked with an empty list'
200             def jsonData = '{"branch": []}'
201             objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
202         then: 'invalid data exception is thrown'
203             thrown(DataValidationException)
204     }
205
206     def 'Get all data nodes #scenario.'() {
207         given: 'persistence service returns data for GET request'
208             mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption) >> dataNode
209         expect: 'service returns same data if using same parameters'
210             objectUnderTest.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption) == dataNode
211         where: 'following parameters were used'
212             scenario                                   | xpath   | fetchDescendantsOption                         |   dataNode
213             'with root node xpath and descendants'     | '/'     | FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath-1').build(), new DataNodeBuilder().withXpath('/xpath-2').build()]
214             'with root node xpath and no descendants'  | '/'     | FetchDescendantsOption.OMIT_DESCENDANTS        | [new DataNodeBuilder().withXpath('/xpath-1').build(), new DataNodeBuilder().withXpath('/xpath-2').build()]
215             'with valid xpath and descendants'         | '/xpath'| FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath').build()]
216             'with valid xpath and no descendants'      | '/xpath'| FetchDescendantsOption.OMIT_DESCENDANTS        | [new DataNodeBuilder().withXpath('/xpath').build()]
217     }
218
219     def 'Get all data nodes over multiple xpaths with option #fetchDescendantsOption.'() {
220         def xpath1 = '/xpath-1'
221         def xpath2 = '/xpath-2'
222         def dataNode = [new DataNodeBuilder().withXpath(xpath1).build(), new DataNodeBuilder().withXpath(xpath2).build()]
223         given: 'persistence service returns data for get data request'
224             mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, anchorName, [xpath1, xpath2], fetchDescendantsOption) >> dataNode
225         expect: 'service returns same data if uses same parameters'
226             objectUnderTest.getDataNodesForMultipleXpaths(dataspaceName, anchorName, [xpath1, xpath2], fetchDescendantsOption) == dataNode
227         where: 'all fetch options are supported'
228             fetchDescendantsOption << [FetchDescendantsOption.OMIT_DESCENDANTS, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS]
229     }
230
231     def 'Update data node leaves: #scenario.'() {
232         given: 'schema set for given anchor and dataspace references test-tree model'
233             setupSchemaSetMocks('test-tree.yang')
234         when: 'update data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
235             objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
236         then: 'the persistence service method is invoked with correct parameters'
237             1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, {dataNode -> dataNode.keySet()[0] == expectedNodeXpath})
238         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
239             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
240         and: 'data updated event is sent to notification service'
241             1 * mockNotificationService.processDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp)
242         where: 'following parameters were used'
243             scenario         | parentNodeXpath | jsonData                        || expectedNodeXpath
244             'top level node' | '/'             | '{"test-tree": {"branch": []}}' || '/test-tree'
245             'level 2 node'   | '/test-tree'    | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']'
246     }
247
248     def 'Update list-element data node with : #scenario.'() {
249         given: 'schema set for given anchor and dataspace references bookstore model'
250             setupSchemaSetMocks('bookstore.yang')
251         when: 'update data method is invoked with json data #jsonData and parent node xpath'
252             objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/bookstore/categories[@code=2]',
253                 jsonData, observedTimestamp)
254         then: 'the persistence service method is invoked with correct parameters'
255             thrown(DataValidationException)
256         where: 'following parameters were used'
257             scenario                  | jsonData
258             'multiple expectedLeaves' | '{"code": "01","name": "some-name"}'
259             'one leaf'                | '{"name": "some-name"}'
260     }
261
262     def 'Update data nodes in different containers.' () {
263         given: 'schema set for given dataspace and anchor refers multipleDataTree model'
264             setupSchemaSetMocks('multipleDataTree.yang')
265         and: 'json string with multiple data trees'
266             def parentNodeXpath = '/'
267             def updatedJsonData = '{"first-container":{"a-leaf":"a-new-Value"},"last-container":{"x-leaf":"x-new-value"}}'
268         when: 'update operation is performed on multiple data nodes'
269             objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, updatedJsonData, observedTimestamp)
270         then: 'the persistence service method is invoked with correct parameters'
271             1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, {dataNode -> dataNode.keySet()[index] == expectedNodeXpath})
272         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
273             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
274         and: 'data updated event is sent to notification service'
275             1 * mockNotificationService.processDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp)
276         where: 'the following parameters were used'
277             index | expectedNodeXpath
278             0     | '/first-container'
279             1     | '/last-container'
280     }
281
282     def 'Update Bookstore node leaves and child.' () {
283         given: 'a DMI registry model'
284             setupSchemaSetMocks('bookstore.yang')
285         and: 'json update for a category (parent) and new book (child)'
286             def jsonData = '{"categories":[{"code":01,"name":"Romance","books": [{"title": "new"}]}]}'
287         when: 'update data method is invoked with json data and parent node xpath'
288             objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName, '/bookstore', jsonData, observedTimestamp)
289         then: 'the persistence service method is invoked for the category (parent)'
290             1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName,
291                     {updatedDataNodesPerXPath -> updatedDataNodesPerXPath.keySet()
292                                                 .iterator().next() == "/bookstore/categories[@code='01']"})
293         and: 'the persistence service method is invoked for the new book (child)'
294             1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName,
295                 {updatedDataNodesPerXPath -> updatedDataNodesPerXPath.keySet()
296                     .iterator().next() == "/bookstore/categories[@code='01']/books[@title='new']"})
297         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
298             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
299         and: 'the data updated event is sent to the notification service'
300             1 * mockNotificationService.processDataUpdatedEvent(anchor, '/bookstore', Operation.UPDATE, observedTimestamp)
301     }
302
303     def 'Replace data node using singular data node: #scenario.'() {
304         given: 'schema set for given anchor and dataspace references test-tree model'
305             setupSchemaSetMocks('test-tree.yang')
306         when: 'replace data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
307             objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
308         then: 'the persistence service method is invoked with correct parameters'
309             1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
310                     { dataNode -> dataNode.xpath == expectedNodeXpath})
311         and: 'data updated event is sent to notification service'
312             1 * mockNotificationService.processDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp)
313         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
314             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
315         where: 'following parameters were used'
316             scenario         | parentNodeXpath | jsonData                                           || expectedNodeXpath
317             'top level node' | '/'             | '{"test-tree": {"branch": []}}'                    || ['/test-tree']
318             'level 2 node'   | '/test-tree'    | '{"branch": [{"name":"Name"}]}'                    || ['/test-tree/branch[@name=\'Name\']']
319             'json list'      | '/test-tree'    | '{"branch": [{"name":"Name1"}, {"name":"Name2"}]}' || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"]
320     }
321
322     def 'Replace data node using multiple data nodes: #scenario.'() {
323         given: 'schema set for given anchor and dataspace references test-tree model'
324             setupSchemaSetMocks('test-tree.yang')
325         when: 'replace data method is invoked with a map of xpaths and json data'
326             objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, nodesJsonData, observedTimestamp)
327         then: 'the persistence service method is invoked with correct parameters'
328             1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
329                 { dataNode -> dataNode.xpath == expectedNodeXpath})
330         and: 'data updated event is sent to notification service'
331             nodesJsonData.keySet().each {
332                 1 * mockNotificationService.processDataUpdatedEvent(anchor, it, Operation.UPDATE, observedTimestamp)
333             }
334         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
335             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
336         where: 'following parameters were used'
337             scenario         | nodesJsonData                                                                                                        || expectedNodeXpath
338             'top level node' | ['/' : '{"test-tree": {"branch": []}}', '/test-tree' : '{"branch": [{"name":"Name"}]}']                              || ["/test-tree", "/test-tree/branch[@name='Name']"]
339             '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"]
340             'json list'      | ['/test-tree' : '{"branch": [{"name":"Name1"}, {"name":"Name2"}]}']                                                  || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"]
341     }
342
343     def 'Replace data node with concurrency exception in persistence layer.'() {
344         given: 'the persistence layer throws an concurrency exception'
345             def originalException = new ConcurrencyException('message', 'details')
346             mockCpsDataPersistenceService.updateDataNodesAndDescendants(*_) >> { throw originalException }
347             setupSchemaSetMocks('test-tree.yang')
348         when: 'attempt to replace data node'
349             objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, ['/' : '{"test-tree": {}}'] , observedTimestamp)
350         then: 'the same exception is thrown up'
351             def thrownUp = thrown(ConcurrencyException)
352             assert thrownUp == originalException
353     }
354
355     def 'Replace list content data fragment under parent node.'() {
356         given: 'schema set for given anchor and dataspace references test-tree model'
357             setupSchemaSetMocks('test-tree.yang')
358         when: 'replace list data method is invoked with list element json data'
359             def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
360             objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
361         then: 'the persistence service method is invoked with correct parameters'
362             1 * mockCpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, '/test-tree',
363                 { dataNodeCollection ->
364                     {
365                         assert dataNodeCollection.size() == 2
366                         assert dataNodeCollection.collect { it.getXpath() }
367                             .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
368                     }
369                 }
370             )
371         and: 'the CpsValidator is called on the dataspaceName and AnchorName twice'
372             2 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
373         and: 'data updated event is sent to notification service'
374             1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree', Operation.UPDATE, observedTimestamp)
375     }
376
377     def 'Replace whole list content with empty list element.'() {
378         given: 'schema set for given anchor and dataspace references test-tree model'
379             setupSchemaSetMocks('test-tree.yang')
380         when: 'replace list data method is invoked with empty list'
381             def jsonData = '{"branch": []}'
382             objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
383         then: 'invalid data exception is thrown'
384             thrown(DataValidationException)
385     }
386
387     def 'Delete list element under existing node.'() {
388         when: 'delete list data method is invoked with list element json data'
389             objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp)
390         then: 'the persistence service method is invoked with correct parameters'
391             1 * mockCpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, '/test-tree/branch')
392         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
393             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
394         and: 'data updated event is sent to notification service'
395             1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree/branch', Operation.DELETE, observedTimestamp)
396     }
397
398     def 'Delete multiple list elements under existing node.'() {
399         when: 'delete multiple list data method is invoked with list element json data'
400             objectUnderTest.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'], observedTimestamp)
401         then: 'the persistence service method is invoked with correct parameters'
402             1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'])
403         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
404             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
405         and: 'two data updated events are sent to notification service'
406             2 * mockNotificationService.processDataUpdatedEvent(anchor, _, Operation.DELETE, observedTimestamp)
407     }
408
409     def 'Delete data node under anchor and dataspace.'() {
410         when: 'delete data node method is invoked with correct parameters'
411             objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
412         then: 'the persistence service method is invoked with the correct parameters'
413             1 * mockCpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, '/data-node')
414         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
415             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
416         and: 'data updated event is sent to notification service'
417             1 * mockNotificationService.processDataUpdatedEvent(anchor, '/data-node', Operation.DELETE, observedTimestamp)
418     }
419
420     def 'Delete all data nodes for a given anchor and dataspace.'() {
421         when: 'delete data nodes method is invoked with correct parameters'
422             objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
423         then: 'data updated event is sent to notification service before the delete'
424             1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.DELETE, observedTimestamp)
425         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
426             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
427         and: 'the persistence service method is invoked with the correct parameters'
428             1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName)
429     }
430
431     def 'Delete all data nodes for a given anchor and dataspace with batch exception in persistence layer.'() {
432         given: 'a batch exception in persistence layer'
433             def originalException = new DataNodeNotFoundExceptionBatch('ds1','a1',[])
434             mockCpsDataPersistenceService.deleteDataNodes(*_)  >> { throw originalException }
435         when: 'attempt to delete data nodes'
436             objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
437         then: 'the original exception is thrown up'
438             def thrownUp = thrown(DataNodeNotFoundExceptionBatch)
439             assert thrownUp == originalException
440         and: 'the exception details contain the expected data'
441             assert thrownUp.details.contains('ds1')
442             assert thrownUp.details.contains('a1')
443     }
444
445     def 'Delete all data nodes for given dataspace and multiple anchors.'() {
446         given: 'schema set for given anchors and dataspace references test tree model'
447             setupSchemaSetMocks('test-tree.yang')
448             mockCpsAdminService.getAnchors(dataspaceName, ['anchor1', 'anchor2']) >>
449                 [new Anchor(name: 'anchor1', dataspaceName: dataspaceName),
450                  new Anchor(name: 'anchor2', dataspaceName: dataspaceName)]
451         when: 'delete data node method is invoked with correct parameters'
452             objectUnderTest.deleteDataNodes(dataspaceName, ['anchor1', 'anchor2'], observedTimestamp)
453         then: 'data updated events are sent to notification service before the delete'
454             2 * mockNotificationService.processDataUpdatedEvent(_, '/', Operation.DELETE, observedTimestamp)
455         and: 'the CpsValidator is called on the dataspace name and the anchor names'
456             2 * mockCpsValidator.validateNameCharacters(_)
457         and: 'the persistence service method is invoked with the correct parameters'
458             1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, _ as Collection<String>)
459     }
460
461     def 'Start session.'() {
462         when: 'start session method is called'
463             objectUnderTest.startSession()
464         then: 'the persistence service method to start session is invoked'
465             1 * mockCpsDataPersistenceService.startSession()
466     }
467
468     def 'Start session with Session Manager Exceptions.'() {
469         given: 'the persistence layer throws an Session Manager Exception'
470             mockCpsDataPersistenceService.startSession() >> { throw originalException }
471         when: 'attempt to start session'
472             objectUnderTest.startSession()
473         then: 'the original exception is thrown up'
474             def thrownUp = thrown(SessionManagerException)
475             assert thrownUp == originalException
476         where: 'variations of Session Manager Exception are used'
477             originalException << [ new SessionManagerException('message','details'),
478                                    new SessionManagerException('message','details', new Exception('cause')),
479                                    new SessionTimeoutException('message','details', new Exception('cause'))]
480     }
481
482     def 'Close session.'(){
483         given: 'session Id from calling the start session method'
484             def sessionId = objectUnderTest.startSession()
485         when: 'close session method is called'
486             objectUnderTest.closeSession(sessionId)
487         then: 'the persistence service method to close session is invoked'
488             1 * mockCpsDataPersistenceService.closeSession(sessionId)
489     }
490
491     def 'Lock anchor with no timeout parameter.'(){
492         when: 'lock anchor method with no timeout parameter with details of anchor entity to lock'
493             objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName')
494         then: 'the persistence service method to lock anchor is invoked with default timeout'
495             1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 300L)
496     }
497
498     def 'Lock anchor with timeout parameter.'(){
499         when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock'
500             objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
501         then: 'the persistence service method to lock anchor is invoked with the given timeout'
502             1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
503     }
504
505     def setupSchemaSetMocks(String... yangResources) {
506         def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
507         mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
508         def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
509         def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
510         mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
511     }
512
513 }