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