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