Introduce Instrumentation
[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 TechMahindra Ltd.
7  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
8  *  Licensed under the Apache License, Version 2.0 (the "License");
9  *  you may not use this file except in compliance with the License.
10  *  You may obtain a copy of the License at
11  *
12  *        http://www.apache.org/licenses/LICENSE-2.0
13  *
14  *  Unless required by applicable law or agreed to in writing, software
15  *  distributed under the License is distributed on an "AS IS" BASIS,
16  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  *  See the License for the specific language governing permissions and
18  *  limitations under the License.
19  *
20  *  SPDX-License-Identifier: Apache-2.0
21  *  ============LICENSE_END=========================================================
22  */
23
24 package org.onap.cps.api.impl
25
26 import org.onap.cps.TestUtils
27 import org.onap.cps.api.CpsAdminService
28 import org.onap.cps.notification.NotificationService
29 import org.onap.cps.notification.Operation
30 import org.onap.cps.spi.CpsDataPersistenceService
31 import org.onap.cps.spi.FetchDescendantsOption
32 import org.onap.cps.spi.exceptions.DataValidationException
33 import org.onap.cps.spi.model.Anchor
34 import org.onap.cps.spi.model.DataNode
35 import org.onap.cps.spi.model.DataNodeBuilder
36 import org.onap.cps.utils.ContentType
37 import org.onap.cps.utils.TimedYangParser
38 import org.onap.cps.yang.YangTextSchemaSourceSet
39 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
40 import spock.lang.Specification
41 import org.onap.cps.spi.utils.CpsValidator
42
43 import java.time.OffsetDateTime
44 import java.util.stream.Collectors
45
46 class CpsDataServiceImplSpec extends Specification {
47     def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService)
48     def mockCpsAdminService = Mock(CpsAdminService)
49     def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)
50     def mockNotificationService = Mock(NotificationService)
51     def mockCpsValidator = Mock(CpsValidator)
52     def timedYangParser = new TimedYangParser()
53
54     def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAdminService,
55             mockYangTextSchemaSourceSetCache, mockNotificationService, mockCpsValidator, timedYangParser)
56
57     def setup() {
58         mockCpsAdminService.getAnchor(dataspaceName, anchorName) >> anchor
59     }
60
61     def dataspaceName = 'some-dataspace'
62     def anchorName = 'some-anchor'
63     def schemaSetName = 'some-schema-set'
64     def anchor = Anchor.builder().name(anchorName).schemaSetName(schemaSetName).build()
65     def observedTimestamp = OffsetDateTime.now()
66
67     def 'Saving multicontainer json data.'() {
68         given: 'schema set for given anchor and dataspace references test-tree model'
69             setupSchemaSetMocks('multipleDataTree.yang')
70         when: 'save data method is invoked with test-tree json data'
71             def jsonData = TestUtils.getResourceFileContent('multiple-object-data.json')
72             objectUnderTest.saveData(dataspaceName, anchorName, jsonData, observedTimestamp)
73         then: 'the persistence service method is invoked with correct parameters'
74             1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
75                 { dataNode -> dataNode.xpath[index] == xpath })
76         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
77             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
78         and: 'data updated event is sent to notification service'
79             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/', Operation.CREATE, observedTimestamp)
80         where:
81             index   |   xpath
82                 0   | '/first-container'
83                 1   | '/last-container'
84
85     }
86
87     def 'Saving #scenario data.'() {
88         given: 'schema set for given anchor and dataspace references test-tree model'
89             setupSchemaSetMocks('test-tree.yang')
90         when: 'save data method is invoked with test-tree #scenario data'
91             def data = TestUtils.getResourceFileContent(dataFile)
92             objectUnderTest.saveData(dataspaceName, anchorName, data, observedTimestamp, contentType)
93         then: 'the persistence service method is invoked with correct parameters'
94             1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
95                     { dataNode -> dataNode.xpath[0] == '/test-tree' })
96         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
97             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
98         and: 'data updated event is sent to notification service'
99             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/', Operation.CREATE, observedTimestamp)
100         where: 'given parameters'
101             scenario | dataFile         | contentType
102             'json'   | 'test-tree.json' | ContentType.JSON
103             'xml'    | 'test-tree.xml'  | ContentType.XML
104     }
105
106     def 'Saving #scenarioDesired data with invalid data.'() {
107         given: 'schema set for given anchor and dataspace references test-tree model'
108         setupSchemaSetMocks('test-tree.yang')
109         when: 'save data method is invoked with test-tree json data'
110             objectUnderTest.saveData(dataspaceName, anchorName, invalidData, observedTimestamp, contentType)
111         then: 'a data validation exception is thrown'
112             thrown(DataValidationException)
113         where: 'given parameters'
114             scenarioDesired | invalidData             | contentType
115             'json'          | '{invalid  json'        | ContentType.XML
116             'xml'           | '<invalid xml'          | ContentType.JSON
117     }
118
119
120     def 'Saving child data fragment under existing node.'() {
121         given: 'schema set for given anchor and dataspace references test-tree model'
122             setupSchemaSetMocks('test-tree.yang')
123         when: 'save data method is invoked with test-tree json data'
124             def jsonData = '{"branch": [{"name": "New"}]}'
125             objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
126         then: 'the persistence service method is invoked with correct parameters'
127             1 * mockCpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, '/test-tree',
128                 { dataNode -> dataNode.xpath[0] == '/test-tree/branch[@name=\'New\']' })
129         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
130             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
131         and: 'data updated event is sent to notification service'
132             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/test-tree', Operation.CREATE, observedTimestamp)
133     }
134
135     def 'Saving list element data fragment under existing node.'() {
136         given: 'schema set for given anchor and dataspace references test-tree model'
137             setupSchemaSetMocks('test-tree.yang')
138         when: 'save data method is invoked with list element json data'
139             def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
140             objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
141         then: 'the persistence service method is invoked with correct parameters'
142             1 * mockCpsDataPersistenceService.addListElements(dataspaceName, anchorName, '/test-tree',
143                 { dataNodeCollection ->
144                     {
145                         assert dataNodeCollection.size() == 2
146                         assert dataNodeCollection.collect { it.getXpath() }
147                             .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
148                     }
149                 }
150             )
151         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
152             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
153         and: 'data updated event is sent to notification service'
154             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/test-tree', Operation.UPDATE, observedTimestamp)
155     }
156
157     def 'Saving collection of a batch with data fragment under existing node.'() {
158         given: 'schema set for given anchor and dataspace references test-tree model'
159             setupSchemaSetMocks('test-tree.yang')
160         when: 'save data method is invoked with list element json data'
161             def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
162             objectUnderTest.saveListElementsBatch(dataspaceName, anchorName, '/test-tree', [jsonData], observedTimestamp)
163         then: 'the persistence service method is invoked with correct parameters'
164             1 * mockCpsDataPersistenceService.addMultipleLists(dataspaceName, anchorName, '/test-tree',_) >> {
165                 args -> {
166                     def listElementsCollection = args[3] as Collection<Collection<DataNode>>
167                     assert listElementsCollection.size() == 1
168                     def listOfXpaths = listElementsCollection.stream().flatMap(x -> x.stream()).map(it-> it.xpath).collect(Collectors.toList())
169                     assert listOfXpaths.size() == 2
170                     assert listOfXpaths.containsAll(['/test-tree/branch[@name=\'B\']','/test-tree/branch[@name=\'A\']'])
171                 }
172             }
173         and: 'data updated event is sent to notification service'
174             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/test-tree', Operation.UPDATE, observedTimestamp)
175     }
176
177     def 'Saving empty list element data fragment.'() {
178         given: 'schema set for given anchor and dataspace references test-tree model'
179             setupSchemaSetMocks('test-tree.yang')
180         when: 'save data method is invoked with an empty list'
181             def jsonData = '{"branch": []}'
182             objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
183         then: 'invalid data exception is thrown'
184             thrown(DataValidationException)
185     }
186
187     def 'Get data node with option #fetchDescendantsOption.'() {
188         def xpath = '/xpath'
189         def dataNode = new DataNodeBuilder().withXpath(xpath).build()
190         given: 'persistence service returns data for get data request'
191             mockCpsDataPersistenceService.getDataNode(dataspaceName, anchorName, xpath, fetchDescendantsOption) >> dataNode
192         expect: 'service returns same data if uses same parameters'
193             objectUnderTest.getDataNode(dataspaceName, anchorName, xpath, fetchDescendantsOption) == dataNode
194         where: 'all fetch options are supported'
195             fetchDescendantsOption << [FetchDescendantsOption.OMIT_DESCENDANTS, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS]
196     }
197
198     def 'Update data node leaves: #scenario.'() {
199         given: 'schema set for given anchor and dataspace references test-tree model'
200             setupSchemaSetMocks('test-tree.yang')
201         when: 'update data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
202             objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
203         then: 'the persistence service method is invoked with correct parameters'
204             1 * mockCpsDataPersistenceService.updateDataLeaves(dataspaceName, anchorName, expectedNodeXpath, leaves)
205         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
206             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
207         and: 'data updated event is sent to notification service'
208             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, parentNodeXpath, Operation.UPDATE, observedTimestamp)
209         where: 'following parameters were used'
210             scenario         | parentNodeXpath | jsonData                        || expectedNodeXpath                   | leaves
211             'top level node' | '/'             | '{"test-tree": {"branch": []}}' || '/test-tree'                        | Collections.emptyMap()
212             'level 2 node'   | '/test-tree'    | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']' | ['name': 'Name']
213     }
214
215     def 'Update list-element data node with : #scenario.'() {
216         given: 'schema set for given anchor and dataspace references bookstore model'
217             setupSchemaSetMocks('bookstore.yang')
218         when: 'update data method is invoked with json data #jsonData and parent node xpath'
219             objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/bookstore/categories[@code=2]',
220                 jsonData, observedTimestamp)
221         then: 'the persistence service method is invoked with correct parameters'
222             thrown(DataValidationException)
223         where: 'following parameters were used'
224             scenario          | jsonData
225             'multiple expectedLeaves' | '{"code": "01","name": "some-name"}'
226             'one leaf'        | '{"name": "some-name"}'
227     }
228
229     def 'Update Bookstore node leaves' () {
230         given: 'a DMI registry model'
231             setupSchemaSetMocks('bookstore.yang')
232         and: 'the expected json string'
233             def jsonData = '{"categories":[{"code":01,"name":"Romance"}]}'
234         when: 'update data method is invoked with json data and parent node xpath'
235             objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName,
236                 '/bookstore', jsonData, observedTimestamp)
237         then: 'the persistence service method is invoked with correct parameters'
238             1 * mockCpsDataPersistenceService.updateDataLeaves(dataspaceName, anchorName,
239                 "/bookstore/categories[@code='01']", ['name':'Romance', 'code': '01'])
240         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
241             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
242         and: 'the data updated event is sent to the notification service'
243             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/bookstore', Operation.UPDATE, observedTimestamp)
244     }
245
246     def 'Replace data node using singular data node: #scenario.'() {
247         given: 'schema set for given anchor and dataspace references test-tree model'
248             setupSchemaSetMocks('test-tree.yang')
249         when: 'replace data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
250             objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
251         then: 'the persistence service method is invoked with correct parameters'
252             1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
253                 { dataNode -> dataNode.xpath[0] == expectedNodeXpath })
254         and: 'data updated event is sent to notification service'
255             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, parentNodeXpath, Operation.UPDATE, observedTimestamp)
256         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
257             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
258         where: 'following parameters were used'
259             scenario         | parentNodeXpath | jsonData                        || expectedNodeXpath
260             'top level node' | '/'             | '{"test-tree": {"branch": []}}' || '/test-tree'
261             'level 2 node'   | '/test-tree'    | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']'
262     }
263
264     def 'Replace data node using multiple data nodes: #scenario.'() {
265         given: 'schema set for given anchor and dataspace references test-tree model'
266             setupSchemaSetMocks('test-tree.yang')
267         when: 'replace data method is invoked with a map of xpaths and json data'
268             objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, nodesJsonData, observedTimestamp)
269         then: 'the persistence service method is invoked with correct parameters'
270             1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
271                 { dataNode -> dataNode.xpath == expectedNodeXpath})
272         and: 'data updated event is sent to notification service'
273             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, nodesJsonData.keySet()[0], Operation.UPDATE, observedTimestamp)
274             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, nodesJsonData.keySet()[1], Operation.UPDATE, observedTimestamp)
275         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
276             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
277         where: 'following parameters were used'
278             scenario         | nodesJsonData                                                                                                        || expectedNodeXpath
279             'top level node' | ['/' : '{"test-tree": {"branch": []}}', '/test-tree' : '{"branch": [{"name":"Name"}]}']                              || ["/test-tree", "/test-tree/branch[@name='Name']"]
280             '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"]
281     }
282
283     def 'Replace list content data fragment under parent node.'() {
284         given: 'schema set for given anchor and dataspace references test-tree model'
285             setupSchemaSetMocks('test-tree.yang')
286         when: 'replace list data method is invoked with list element json data'
287             def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
288             objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
289         then: 'the persistence service method is invoked with correct parameters'
290             1 * mockCpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, '/test-tree',
291                 { dataNodeCollection ->
292                     {
293                         assert dataNodeCollection.size() == 2
294                         assert dataNodeCollection.collect { it.getXpath() }
295                             .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']'])
296                     }
297                 }
298             )
299         and: 'the CpsValidator is called on the dataspaceName and AnchorName twice'
300             2 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
301         and: 'data updated event is sent to notification service'
302             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/test-tree', Operation.UPDATE, observedTimestamp)
303     }
304
305     def 'Replace whole list content with empty list element.'() {
306         given: 'schema set for given anchor and dataspace references test-tree model'
307             setupSchemaSetMocks('test-tree.yang')
308         when: 'replace list data method is invoked with empty list'
309             def jsonData = '{"branch": []}'
310             objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
311         then: 'invalid data exception is thrown'
312             thrown(DataValidationException)
313     }
314
315     def 'Delete list element under existing node.'() {
316         given: 'schema set for given anchor and dataspace references test-tree model'
317             setupSchemaSetMocks('test-tree.yang')
318         when: 'delete list data method is invoked with list element json data'
319             objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp)
320         then: 'the persistence service method is invoked with correct parameters'
321             1 * mockCpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, '/test-tree/branch')
322         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
323             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
324         and: 'data updated event is sent to notification service'
325             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/test-tree/branch', Operation.DELETE, observedTimestamp)
326     }
327
328     def 'Delete data node under anchor and dataspace.'() {
329         given: 'schema set for given anchor and dataspace references test tree model'
330             setupSchemaSetMocks('test-tree.yang')
331         when: 'delete data node method is invoked with correct parameters'
332             objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
333         then: 'the persistence service method is invoked with the correct parameters'
334             1 * mockCpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, '/data-node')
335         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
336             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
337         and: 'data updated event is sent to notification service'
338             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/data-node', Operation.DELETE, observedTimestamp)
339     }
340
341     def 'Delete all data nodes for a given anchor and dataspace.'() {
342         given: 'schema set for given anchor and dataspace references test tree model'
343             setupSchemaSetMocks('test-tree.yang')
344         when: 'delete data node method is invoked with correct parameters'
345             objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
346         then: 'data updated event is sent to notification service before the delete'
347             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/', Operation.DELETE, observedTimestamp)
348         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
349             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
350         and: 'the persistence service method is invoked with the correct parameters'
351             1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName)
352     }
353
354     def setupSchemaSetMocks(String... yangResources) {
355         def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
356         mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
357         def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
358         def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
359         mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
360     }
361
362     def 'start session'() {
363         when: 'start session method is called'
364             objectUnderTest.startSession()
365         then: 'the persistence service method to start session is invoked'
366             1 * mockCpsDataPersistenceService.startSession()
367     }
368
369     def 'close session'(){
370         given: 'session Id from calling the start session method'
371             def sessionId = objectUnderTest.startSession()
372         when: 'close session method is called'
373             objectUnderTest.closeSession(sessionId)
374         then: 'the persistence service method to close session is invoked'
375             1 * mockCpsDataPersistenceService.closeSession(sessionId)
376     }
377
378     def 'lock anchor with no timeout parameter'(){
379         when: 'lock anchor method with no timeout parameter with details of anchor entity to lock'
380             objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName')
381         then: 'the persistence service method to lock anchor is invoked with default timeout'
382             1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName',
383                     'some-anchorName', 300L)
384     }
385
386     def 'lock anchor with timeout parameter'(){
387         when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock'
388             objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName',
389                     'some-anchorName', 250L)
390         then: 'the persistence service method to lock anchor is invoked with the given timeout'
391             1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName',
392                     'some-anchorName', 250L)
393     }
394 }