Creation of DataNodeBuilder with module name prefix is very slow
[cps.git] / cps-ri / src / test / groovy / org / onap / cps / spi / impl / CpsDataPersistenceServiceSpec.groovy
1 /*
2  * ============LICENSE_START=======================================================
3  * Copyright (c) 2021 Bell Canada.
4  * Modifications Copyright (C) 2021-2022 Nordix Foundation
5  * ================================================================================
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *        http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  * ============LICENSE_END=========================================================
18 */
19
20 package org.onap.cps.spi.impl
21
22 import com.fasterxml.jackson.databind.ObjectMapper
23 import org.hibernate.StaleStateException
24 import org.onap.cps.spi.FetchDescendantsOption
25 import org.onap.cps.spi.cache.AnchorDataCacheEntry
26 import org.onap.cps.spi.entities.AnchorEntity
27 import org.onap.cps.spi.entities.FragmentEntity
28 import org.onap.cps.spi.entities.SchemaSetEntity
29 import org.onap.cps.spi.entities.YangResourceEntity
30 import org.onap.cps.spi.exceptions.ConcurrencyException
31 import org.onap.cps.spi.exceptions.DataValidationException
32 import org.onap.cps.spi.model.DataNode
33 import org.onap.cps.spi.model.DataNodeBuilder
34 import org.onap.cps.spi.repository.AnchorRepository
35 import org.onap.cps.spi.repository.DataspaceRepository
36 import org.onap.cps.spi.repository.FragmentRepository
37 import org.onap.cps.spi.utils.SessionManager
38 import org.onap.cps.utils.JsonObjectMapper
39 import spock.lang.Shared
40 import spock.lang.Specification
41 import com.hazelcast.map.IMap;
42
43 class CpsDataPersistenceServiceSpec extends Specification {
44
45     def mockDataspaceRepository = Mock(DataspaceRepository)
46     def mockAnchorRepository = Mock(AnchorRepository)
47     def mockFragmentRepository = Mock(FragmentRepository)
48     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
49     def mockSessionManager = Mock(SessionManager)
50     def mockAnchorDataCache =  Mock(IMap<String, AnchorDataCacheEntry>)
51
52     def objectUnderTest = new CpsDataPersistenceServiceImpl(
53             mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper, mockSessionManager, mockAnchorDataCache)
54
55     @Shared
56     def NEW_RESOURCE_CONTENT = 'module stores {\n' +
57             '    yang-version 1.1;\n' +
58             '    namespace "org:onap:ccsdk:sample";\n' +
59             '\n' +
60             '    prefix book-store;\n' +
61             '\n' +
62             '    revision "2020-09-15" {\n' +
63             '        description\n' +
64             '        "Sample Model";\n' +
65             '    }' +
66             '}'
67
68     @Shared
69     def yangResourceSet = [new YangResourceEntity(moduleName: 'moduleName', content: NEW_RESOURCE_CONTENT,
70             fileName: 'sampleYangResource'
71     )] as Set
72
73
74     def 'Handling of StaleStateException (caused by concurrent updates) during update data node and descendants.'() {
75         given: 'the fragment repository returns a fragment entity'
76             mockFragmentRepository.getByDataspaceAndAnchorAndXpath(*_) >> {
77                 def fragmentEntity = new FragmentEntity()
78                 fragmentEntity.setChildFragments([new FragmentEntity()] as Set<FragmentEntity>)
79                 return fragmentEntity
80             }
81         and: 'a data node is concurrently updated by another transaction'
82             mockFragmentRepository.save(_) >> { throw new StaleStateException("concurrent updates") }
83         when: 'attempt to update data node with submitted data nodes'
84             objectUnderTest.updateDataNodeAndDescendants('some-dataspace', 'some-anchor', new DataNodeBuilder().withXpath('/some/xpath').build())
85         then: 'concurrency exception is thrown'
86             def concurrencyException = thrown(ConcurrencyException)
87             assert concurrencyException.getDetails().contains('some-dataspace')
88             assert concurrencyException.getDetails().contains('some-anchor')
89             assert concurrencyException.getDetails().contains('/some/xpath')
90     }
91
92     def 'Handling of StaleStateException (caused by concurrent updates) during update data nodes and descendants.'() {
93         given: 'the system contains and can update one datanode'
94             def dataNode1 = mockDataNodeAndFragmentEntity('/node1', 'OK')
95         and: 'the system contains two more datanodes that throw an exception while updating'
96             def dataNode2 = mockDataNodeAndFragmentEntity('/node2', 'EXCEPTION')
97             def dataNode3 = mockDataNodeAndFragmentEntity('/node3', 'EXCEPTION')
98         and: 'the batch update will therefore also fail'
99             mockFragmentRepository.saveAll(*_) >> { throw new StaleStateException("concurrent updates") }
100         when: 'attempt batch update data nodes'
101             objectUnderTest.updateDataNodesAndDescendants('some-dataspace', 'some-anchor', [dataNode1, dataNode2, dataNode3])
102         then: 'concurrency exception is thrown'
103             def thrown = thrown(ConcurrencyException)
104             assert thrown.message == 'Concurrent Transactions'
105         and: 'it does not contain the successfull datanode'
106             assert !thrown.details.contains('/node1')
107         and: 'it contains the failed datanodes'
108             assert thrown.details.contains('/node2')
109             assert thrown.details.contains('/node3')
110
111     }
112
113     def 'Retrieving a data node with a property JSON value of #scenario'() {
114         given: 'a fragment with a property JSON value of #scenario'
115         mockFragmentRepository.getByDataspaceAndAnchorAndXpath(*_) >> {
116             new FragmentEntity(childFragments: Collections.emptySet(),
117                     attributes: "{\"some attribute\": ${dataString}}",
118                     anchor: new AnchorEntity(schemaSet: new SchemaSetEntity(yangResources: yangResourceSet )))
119         }
120         when: 'getting the data node represented by this fragment'
121             def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
122                     '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
123         then: 'the leaf is of the correct value and data type'
124             def attributeValue = dataNode.leaves.get('some attribute')
125             assert attributeValue == expectedValue
126             assert attributeValue.class == expectedDataClass
127         where: 'the following Data Type is passed'
128             scenario                              | dataString            || expectedValue     | expectedDataClass
129             'just numbers'                        | '15174'               || 15174             | Integer
130             'number with dot'                     | '15174.32'            || 15174.32          | Double
131             'number with 0 value after dot'       | '15174.0'             || 15174.0           | Double
132             'number with 0 value before dot'      | '0.32'                || 0.32              | Double
133             'number higher than max int'          | '2147483648'          || 2147483648        | Long
134             'just text'                           | '"Test"'              || 'Test'            | String
135             'number with exponent'                | '1.2345e5'            || 1.2345e5          | Double
136             'number higher than max int with dot' | '123456789101112.0'   || 123456789101112.0 | Double
137             'text and numbers'                    | '"String = \'1234\'"' || "String = '1234'" | String
138             'number as String'                    | '"12345"'             || '12345'           | String
139     }
140
141     def 'Retrieving a data node with invalid JSON'() {
142         given: 'a fragment with invalid JSON'
143             mockFragmentRepository.getByDataspaceAndAnchorAndXpath(*_) >> {
144                 new FragmentEntity(childFragments: Collections.emptySet(), attributes: '{invalid json')
145             }
146         when: 'getting the data node represented by this fragment'
147             objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
148                     '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
149         then: 'a data validation exception is thrown'
150             thrown(DataValidationException)
151     }
152
153     def 'start session'() {
154         when: 'start session'
155             objectUnderTest.startSession()
156         then: 'the session manager method to start session is invoked'
157             1 * mockSessionManager.startSession()
158     }
159
160     def 'close session'() {
161         given: 'session ID'
162             def someSessionId = 'someSessionId'
163         when: 'close session method is called with session ID as parameter'
164             objectUnderTest.closeSession(someSessionId)
165         then: 'the session manager method to close session is invoked with parameter'
166             1 * mockSessionManager.closeSession(someSessionId, mockSessionManager.WITH_COMMIT)
167     }
168
169     def 'Lock anchor.'(){
170         when: 'lock anchor method is called with anchor entity details'
171             objectUnderTest.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L)
172         then: 'the session manager method to lock anchor is invoked with same parameters'
173             1 * mockSessionManager.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L)
174     }
175
176     def 'update data node and descendants: #scenario'(){
177         given: 'mocked responses'
178             mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/test/xpath') >> new FragmentEntity(xpath: '/test/xpath', childFragments: [])
179         when: 'replace data node tree'
180             objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', dataNodes)
181         then: 'call fragment repository save all method'
182             1 * mockFragmentRepository.saveAll({fragmentEntities -> assert fragmentEntities as List == expectedFragmentEntities})
183         where: 'the following Data Type is passed'
184             scenario                         | dataNodes                                                                          || expectedFragmentEntities
185             'empty data node list'           | []                                                                                 || []
186             'one data node in list'          | [new DataNode(xpath: '/test/xpath', leaves: ['id': 'testId'], childDataNodes: [])] || [new FragmentEntity(xpath: '/test/xpath', attributes: '{"id":"testId"}', childFragments: [])]
187     }
188
189     def 'update data nodes and descendants'() {
190         given: 'the fragment repository returns a fragment entity related to the xpath input'
191             mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/test/xpath1') >> new FragmentEntity(xpath: '/test/xpath1', childFragments: [])
192             mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/test/xpath2') >> new FragmentEntity(xpath: '/test/xpath2', childFragments: [])
193         and: 'some data nodes with descendants'
194             def dataNode1 = new DataNode(xpath: '/test/xpath1', leaves: ['id': 'testId1'], childDataNodes: [new DataNode(xpath: '/test/xpath1/child', leaves: ['id': 'childTestId1'])])
195             def dataNode2 = new DataNode(xpath: '/test/xpath2', leaves: ['id': 'testId2'], childDataNodes: [new DataNode(xpath: '/test/xpath2/child', leaves: ['id': 'childTestId2'])])
196         when: 'the fragment entities are update by the data nodes'
197             objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', [dataNode1, dataNode2])
198         then: 'call fragment repository save all method is called with the updated fragments'
199             1 * mockFragmentRepository.saveAll({fragmentEntities -> {
200                 fragmentEntities.containsAll([
201                     new FragmentEntity(xpath: '/test/xpath1', attributes: '{"id":"testId1"}', childFragments: [new FragmentEntity(xpath: '/test/xpath1/child', attributes: '{"id":"childTestId1"}', childFragments: [])]),
202                     new FragmentEntity(xpath: '/test/xpath2', attributes: '{"id":"testId2"}', childFragments: [new FragmentEntity(xpath: '/test/xpath2/child', attributes: '{"id":"childTestId2"}', childFragments: [])])
203                 ])
204                 assert fragmentEntities.size() == 2
205             }})
206     }
207
208     def mockDataNodeAndFragmentEntity(xpath, scenario) {
209         def dataNode = new DataNodeBuilder().withXpath(xpath).build()
210         def fragmentEntity = new FragmentEntity(xpath: xpath, childFragments: [])
211         mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, xpath) >> fragmentEntity
212         if ('EXCEPTION' == scenario) {
213             mockFragmentRepository.save(fragmentEntity) >> { throw new StaleStateException("concurrent updates") }
214         }
215         return dataNode
216     }
217 }