Merge "Fix: Make bookstore data consistent"
[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[0] == 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     }
320
321     def 'Replace data node using multiple data nodes: #scenario.'() {
322         given: 'schema set for given anchor and dataspace references test-tree model'
323             setupSchemaSetMocks('test-tree.yang')
324         when: 'replace data method is invoked with a map of xpaths and json data'
325             objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, nodesJsonData, observedTimestamp)
326         then: 'the persistence service method is invoked with correct parameters'
327             1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
328                 { dataNode -> dataNode.xpath == expectedNodeXpath})
329         and: 'data updated event is sent to notification service'
330             1 * mockNotificationService.processDataUpdatedEvent(anchor, nodesJsonData.keySet()[0], Operation.UPDATE, observedTimestamp)
331             1 * mockNotificationService.processDataUpdatedEvent(anchor, nodesJsonData.keySet()[1], Operation.UPDATE, observedTimestamp)
332         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
333             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
334         where: 'following parameters were used'
335             scenario         | nodesJsonData                                                                                                        || expectedNodeXpath
336             'top level node' | ['/' : '{"test-tree": {"branch": []}}', '/test-tree' : '{"branch": [{"name":"Name"}]}']                              || ["/test-tree", "/test-tree/branch[@name='Name']"]
337             '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"]
338     }
339
340     def 'Replace data node with concurrency exception in persistence layer.'() {
341         given: 'the persistence layer throws an concurrency exception'
342             def originalException = new ConcurrencyException('message', 'details')
343             mockCpsDataPersistenceService.updateDataNodesAndDescendants(*_) >> { throw originalException }
344             setupSchemaSetMocks('test-tree.yang')
345         when: 'attempt to replace data node'
346             objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, ['/' : '{"test-tree": {}}'] , observedTimestamp)
347         then: 'the same exception is thrown up'
348             def thrownUp = thrown(ConcurrencyException)
349             assert thrownUp == originalException
350     }
351
352     def 'Replace list content data fragment under parent node.'() {
353         given: 'schema set for given anchor and dataspace references test-tree model'
354             setupSchemaSetMocks('test-tree.yang')
355         when: 'replace list data method is invoked with list element json data'
356             def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
357             objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
358         then: 'the persistence service method is invoked with correct parameters'
359             1 * mockCpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, '/test-tree',
360                 { dataNodeCollection ->
361                     {
362                         assert dataNodeCollection.size() == 2
363                         assert dataNodeCollection.collect { it.getXpath() }
364                             .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
365                     }
366                 }
367             )
368         and: 'the CpsValidator is called on the dataspaceName and AnchorName twice'
369             2 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
370         and: 'data updated event is sent to notification service'
371             1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree', Operation.UPDATE, observedTimestamp)
372     }
373
374     def 'Replace whole list content with empty list element.'() {
375         given: 'schema set for given anchor and dataspace references test-tree model'
376             setupSchemaSetMocks('test-tree.yang')
377         when: 'replace list data method is invoked with empty list'
378             def jsonData = '{"branch": []}'
379             objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
380         then: 'invalid data exception is thrown'
381             thrown(DataValidationException)
382     }
383
384     def 'Delete list element under existing node.'() {
385         when: 'delete list data method is invoked with list element json data'
386             objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp)
387         then: 'the persistence service method is invoked with correct parameters'
388             1 * mockCpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, '/test-tree/branch')
389         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
390             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
391         and: 'data updated event is sent to notification service'
392             1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree/branch', Operation.DELETE, observedTimestamp)
393     }
394
395     def 'Delete multiple list elements under existing node.'() {
396         when: 'delete multiple list data method is invoked with list element json data'
397             objectUnderTest.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'], observedTimestamp)
398         then: 'the persistence service method is invoked with correct parameters'
399             1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'])
400         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
401             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
402         and: 'two data updated events are sent to notification service'
403             2 * mockNotificationService.processDataUpdatedEvent(anchor, _, Operation.DELETE, observedTimestamp)
404     }
405
406     def 'Delete data node under anchor and dataspace.'() {
407         when: 'delete data node method is invoked with correct parameters'
408             objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
409         then: 'the persistence service method is invoked with the correct parameters'
410             1 * mockCpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, '/data-node')
411         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
412             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
413         and: 'data updated event is sent to notification service'
414             1 * mockNotificationService.processDataUpdatedEvent(anchor, '/data-node', Operation.DELETE, observedTimestamp)
415     }
416
417     def 'Delete all data nodes for a given anchor and dataspace.'() {
418         when: 'delete data nodes method is invoked with correct parameters'
419             objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
420         then: 'data updated event is sent to notification service before the delete'
421             1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.DELETE, observedTimestamp)
422         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
423             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
424         and: 'the persistence service method is invoked with the correct parameters'
425             1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName)
426     }
427
428     def 'Delete all data nodes for a given anchor and dataspace with batch exception in persistence layer.'() {
429         given: 'a batch exception in persistence layer'
430             def originalException = new DataNodeNotFoundExceptionBatch('ds1','a1',[])
431             mockCpsDataPersistenceService.deleteDataNodes(*_)  >> { throw originalException }
432         when: 'attempt to delete data nodes'
433             objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
434         then: 'the original exception is thrown up'
435             def thrownUp = thrown(DataNodeNotFoundExceptionBatch)
436             assert thrownUp == originalException
437         and: 'the exception details contain the expected data'
438             assert thrownUp.details.contains('ds1')
439             assert thrownUp.details.contains('a1')
440     }
441
442     def 'Delete all data nodes for given dataspace and multiple anchors.'() {
443         given: 'schema set for given anchors and dataspace references test tree model'
444             setupSchemaSetMocks('test-tree.yang')
445             mockCpsAdminService.getAnchors(dataspaceName, ['anchor1', 'anchor2']) >>
446                 [new Anchor(name: 'anchor1', dataspaceName: dataspaceName),
447                  new Anchor(name: 'anchor2', dataspaceName: dataspaceName)]
448         when: 'delete data node method is invoked with correct parameters'
449             objectUnderTest.deleteDataNodes(dataspaceName, ['anchor1', 'anchor2'], observedTimestamp)
450         then: 'data updated events are sent to notification service before the delete'
451             2 * mockNotificationService.processDataUpdatedEvent(_, '/', Operation.DELETE, observedTimestamp)
452         and: 'the CpsValidator is called on the dataspace name and the anchor names'
453             2 * mockCpsValidator.validateNameCharacters(_)
454         and: 'the persistence service method is invoked with the correct parameters'
455             1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, _ as Collection<String>)
456     }
457
458     def 'Start session.'() {
459         when: 'start session method is called'
460             objectUnderTest.startSession()
461         then: 'the persistence service method to start session is invoked'
462             1 * mockCpsDataPersistenceService.startSession()
463     }
464
465     def 'Start session with Session Manager Exceptions.'() {
466         given: 'the persistence layer throws an Session Manager Exception'
467             mockCpsDataPersistenceService.startSession() >> { throw originalException }
468         when: 'attempt to start session'
469             objectUnderTest.startSession()
470         then: 'the original exception is thrown up'
471             def thrownUp = thrown(SessionManagerException)
472             assert thrownUp == originalException
473         where: 'variations of Session Manager Exception are used'
474             originalException << [ new SessionManagerException('message','details'),
475                                    new SessionManagerException('message','details', new Exception('cause')),
476                                    new SessionTimeoutException('message','details', new Exception('cause'))]
477     }
478
479     def 'Close session.'(){
480         given: 'session Id from calling the start session method'
481             def sessionId = objectUnderTest.startSession()
482         when: 'close session method is called'
483             objectUnderTest.closeSession(sessionId)
484         then: 'the persistence service method to close session is invoked'
485             1 * mockCpsDataPersistenceService.closeSession(sessionId)
486     }
487
488     def 'Lock anchor with no timeout parameter.'(){
489         when: 'lock anchor method with no timeout parameter with details of anchor entity to lock'
490             objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName')
491         then: 'the persistence service method to lock anchor is invoked with default timeout'
492             1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 300L)
493     }
494
495     def 'Lock anchor with timeout parameter.'(){
496         when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock'
497             objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
498         then: 'the persistence service method to lock anchor is invoked with the given timeout'
499             1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
500     }
501
502     def setupSchemaSetMocks(String... yangResources) {
503         def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
504         mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
505         def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
506         def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
507         mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
508     }
509
510 }