Merge "Performance Improvement: Batch Update DataNodes"
[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.entities.AnchorEntity
26 import org.onap.cps.spi.entities.FragmentEntity
27 import org.onap.cps.spi.entities.SchemaSetEntity
28 import org.onap.cps.spi.entities.YangResourceEntity
29 import org.onap.cps.spi.exceptions.ConcurrencyException
30 import org.onap.cps.spi.exceptions.DataValidationException
31 import org.onap.cps.spi.model.DataNode
32 import org.onap.cps.spi.model.DataNodeBuilder
33 import org.onap.cps.spi.repository.AnchorRepository
34 import org.onap.cps.spi.repository.DataspaceRepository
35 import org.onap.cps.spi.repository.FragmentRepository
36 import org.onap.cps.spi.utils.SessionManager
37 import org.onap.cps.utils.JsonObjectMapper
38 import spock.lang.Shared
39 import spock.lang.Specification
40
41 class CpsDataPersistenceServiceSpec extends Specification {
42
43     def mockDataspaceRepository = Mock(DataspaceRepository)
44     def mockAnchorRepository = Mock(AnchorRepository)
45     def mockFragmentRepository = Mock(FragmentRepository)
46     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
47     def mockSessionManager = Mock(SessionManager)
48
49     def objectUnderTest = new CpsDataPersistenceServiceImpl(
50             mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper,mockSessionManager)
51
52     @Shared
53     def NEW_RESOURCE_CONTENT = 'module stores {\n' +
54             '    yang-version 1.1;\n' +
55             '    namespace "org:onap:ccsdk:sample";\n' +
56             '\n' +
57             '    prefix book-store;\n' +
58             '\n' +
59             '    revision "2020-09-15" {\n' +
60             '        description\n' +
61             '        "Sample Model";\n' +
62             '    }' +
63             '}'
64
65     @Shared
66     def yangResourceSet = [new YangResourceEntity(moduleName: 'moduleName', content: NEW_RESOURCE_CONTENT,
67             fileName: 'sampleYangResource'
68     )] as Set
69
70
71     def 'Handling of StaleStateException (caused by concurrent updates) during update data node and descendants.'() {
72         given: 'the fragment repository returns a fragment entity'
73             mockFragmentRepository.getByDataspaceAndAnchorAndXpath(*_) >> {
74                 def fragmentEntity = new FragmentEntity()
75                 fragmentEntity.setChildFragments([new FragmentEntity()] as Set<FragmentEntity>)
76                 return fragmentEntity
77             }
78         and: 'a data node is concurrently updated by another transaction'
79             mockFragmentRepository.save(_) >> { throw new StaleStateException("concurrent updates") }
80         when: 'attempt to update data node with submitted data nodes'
81             objectUnderTest.updateDataNodeAndDescendants('some-dataspace', 'some-anchor', new DataNodeBuilder().withXpath('/some/xpath').build())
82         then: 'concurrency exception is thrown'
83             def concurrencyException = thrown(ConcurrencyException)
84             assert concurrencyException.getDetails().contains('some-dataspace')
85             assert concurrencyException.getDetails().contains('some-anchor')
86             assert concurrencyException.getDetails().contains('/some/xpath')
87     }
88
89     def 'Handling of StaleStateException (caused by concurrent updates) during  update data nodes and descendants.'() {
90         given: 'the fragment repository returns a list of fragment entities'
91             mockFragmentRepository.getByDataspaceAndAnchorAndXpath(*_) >> new FragmentEntity()
92         and: 'a data node is concurrently updated by another transaction'
93             mockFragmentRepository.saveAll(*_) >> { throw new StaleStateException("concurrent updates") }
94         when: 'attempt to update data node with submitted data nodes'
95             objectUnderTest.updateDataNodesAndDescendants('some-dataspace', 'some-anchor', [])
96         then: 'concurrency exception is thrown'
97             def concurrencyException = thrown(ConcurrencyException)
98             assert concurrencyException.getDetails().contains('some-dataspace')
99             assert concurrencyException.getDetails().contains('some-anchor')
100     }
101
102     def 'Retrieving a data node with a property JSON value of #scenario'() {
103         given: 'a fragment with a property JSON value of #scenario'
104         mockFragmentRepository.getByDataspaceAndAnchorAndXpath(*_) >> {
105             new FragmentEntity(childFragments: Collections.emptySet(),
106                     attributes: "{\"some attribute\": ${dataString}}",
107                     anchor: new AnchorEntity(schemaSet: new SchemaSetEntity(yangResources: yangResourceSet )))
108         }
109         when: 'getting the data node represented by this fragment'
110             def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
111                     '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
112         then: 'the leaf is of the correct value and data type'
113             def attributeValue = dataNode.leaves.get('some attribute')
114             assert attributeValue == expectedValue
115             assert attributeValue.class == expectedDataClass
116         where: 'the following Data Type is passed'
117             scenario                              | dataString            || expectedValue     | expectedDataClass
118             'just numbers'                        | '15174'               || 15174             | Integer
119             'number with dot'                     | '15174.32'            || 15174.32          | Double
120             'number with 0 value after dot'       | '15174.0'             || 15174.0           | Double
121             'number with 0 value before dot'      | '0.32'                || 0.32              | Double
122             'number higher than max int'          | '2147483648'          || 2147483648        | Long
123             'just text'                           | '"Test"'              || 'Test'            | String
124             'number with exponent'                | '1.2345e5'            || 1.2345e5          | Double
125             'number higher than max int with dot' | '123456789101112.0'   || 123456789101112.0 | Double
126             'text and numbers'                    | '"String = \'1234\'"' || "String = '1234'" | String
127             'number as String'                    | '"12345"'             || '12345'           | String
128     }
129
130     def 'Retrieving a data node with invalid JSON'() {
131         given: 'a fragment with invalid JSON'
132             mockFragmentRepository.getByDataspaceAndAnchorAndXpath(*_) >> {
133                 new FragmentEntity(childFragments: Collections.emptySet(), attributes: '{invalid json')
134             }
135         when: 'getting the data node represented by this fragment'
136             objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
137                     '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
138         then: 'a data validation exception is thrown'
139             thrown(DataValidationException)
140     }
141
142     def 'start session'() {
143         when: 'start session'
144             objectUnderTest.startSession()
145         then: 'the session manager method to start session is invoked'
146             1 * mockSessionManager.startSession()
147     }
148
149     def 'close session'() {
150         given: 'session ID'
151             def someSessionId = 'someSessionId'
152         when: 'close session method is called with session ID as parameter'
153             objectUnderTest.closeSession(someSessionId)
154         then: 'the session manager method to close session is invoked with parameter'
155             1 * mockSessionManager.closeSession(someSessionId, mockSessionManager.WITH_COMMIT)
156     }
157
158     def 'Lock anchor.'(){
159         when: 'lock anchor method is called with anchor entity details'
160             objectUnderTest.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L)
161         then: 'the session manager method to lock anchor is invoked with same parameters'
162             1 * mockSessionManager.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L)
163     }
164
165     def 'update data node and descendants: #scenario'(){
166         given: 'mocked responses'
167             mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/test/xpath') >> new FragmentEntity(xpath: '/test/xpath', childFragments: [])
168         when: 'replace data node tree'
169             objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', dataNodes)
170         then: 'call fragment repository save all method'
171             1 * mockFragmentRepository.saveAll({fragmentEntities -> assert fragmentEntities as List == expectedFragmentEntities})
172         where: 'the following Data Type is passed'
173             scenario                         | dataNodes                                                                          || expectedFragmentEntities
174             'empty data node list'           | []                                                                                 || []
175             'one data node in list'          | [new DataNode(xpath: '/test/xpath', leaves: ['id': 'testId'], childDataNodes: [])] || [new FragmentEntity(xpath: '/test/xpath', attributes: '{"id":"testId"}', childFragments: [])]
176     }
177
178     def 'update data nodes and descendants'() {
179         given: 'the fragment repository returns a fragment entity related to the xpath input'
180             mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/test/xpath1') >> new FragmentEntity(xpath: '/test/xpath1', childFragments: [])
181             mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/test/xpath2') >> new FragmentEntity(xpath: '/test/xpath2', childFragments: [])
182         and: 'some data nodes with descendants'
183             def dataNode1 = new DataNode(xpath: '/test/xpath1', leaves: ['id': 'testId1'], childDataNodes: [new DataNode(xpath: '/test/xpath1/child', leaves: ['id': 'childTestId1'])])
184             def dataNode2 = new DataNode(xpath: '/test/xpath2', leaves: ['id': 'testId2'], childDataNodes: [new DataNode(xpath: '/test/xpath2/child', leaves: ['id': 'childTestId2'])])
185         when: 'the fragment entities are update by the data nodes'
186             objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', [dataNode1, dataNode2])
187         then: 'call fragment repository save all method is called with the updated fragments'
188             1 * mockFragmentRepository.saveAll({fragmentEntities -> {
189                 fragmentEntities.containsAll([
190                     new FragmentEntity(xpath: '/test/xpath1', attributes: '{"id":"testId1"}', childFragments: [new FragmentEntity(xpath: '/test/xpath1/child', attributes: '{"id":"childTestId1"}', childFragments: [])]),
191                     new FragmentEntity(xpath: '/test/xpath2', attributes: '{"id":"testId2"}', childFragments: [new FragmentEntity(xpath: '/test/xpath2/child', attributes: '{"id":"childTestId2"}', childFragments: [])])
192                 ])
193                 assert fragmentEntities.size() == 2
194             }})
195     }
196 }