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
12 * http://www.apache.org/licenses/LICENSE-2.0
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.
20 * SPDX-License-Identifier: Apache-2.0
21 * ============LICENSE_END=========================================================
24 package org.onap.cps.api.impl
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
50 import java.time.OffsetDateTime
51 import java.util.stream.Collectors
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);
62 def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAdminService,
63 mockYangTextSchemaSourceSetCache, mockNotificationService, mockCpsValidator, timedYangParser, mockCpsDeltaService)
67 mockCpsAdminService.getAnchor(dataspaceName, anchorName) >> anchor
68 mockCpsAdminService.getAnchor(dataspaceName, ANCHOR_NAME_1) >> anchor1
69 mockCpsAdminService.getAnchor(dataspaceName, ANCHOR_NAME_2) >> anchor2
73 static def ANCHOR_NAME_1 = 'some-anchor-1'
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()
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
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'
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'
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 ->
140 assert dataNodeCollection.size() == 1
141 assert dataNodeCollection.collect { it.getXpath() }
142 .containsAll(['/bookstore-address[@bookstore-name=\'Easons\']'])
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)
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)
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 ->
177 assert dataNodeCollection.size() == 2
178 assert dataNodeCollection.collect { it.getXpath() }
179 .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
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)
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',_) >> {
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\']'])
205 and: 'data updated event is sent to notification service'
206 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree', Operation.UPDATE, observedTimestamp)
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)
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()]
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]
244 def 'Get delta between 2 anchors'() {
245 given: 'some xpath, source and target data nodes'
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)
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\']'
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'
287 'multiple expectedLeaves' | '{"code": "01","name": "some-name"}'
288 'one leaf' | '{"name": "some-name"}'
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'
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)
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']"]
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)
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']"]
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
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 ->
394 assert dataNodeCollection.size() == 2
395 assert dataNodeCollection.collect { it.getXpath() }
396 .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
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)
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)
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)
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)
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)
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)
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')
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>)
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()
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'))]
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)
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)
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)
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