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