Read Performance Improvement - Using Native Query
[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.FragmentExtract
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 import com.hazelcast.map.IMap;
41
42 class CpsDataPersistenceServiceSpec extends Specification {
43
44     def mockDataspaceRepository = Mock(DataspaceRepository)
45     def mockAnchorRepository = Mock(AnchorRepository)
46     def mockFragmentRepository = Mock(FragmentRepository)
47     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
48     def mockSessionManager = Mock(SessionManager)
49     def mockAnchorDataCache =  Mock(IMap<String, AnchorDataCacheEntry>)
50
51     def objectUnderTest = new CpsDataPersistenceServiceImpl(
52             mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper, mockSessionManager, mockAnchorDataCache)
53
54     def 'Handling of StaleStateException (caused by concurrent updates) during update data node and descendants.'() {
55         given: 'the fragment repository returns a fragment entity'
56             mockFragmentRepository.getByDataspaceAndAnchorAndXpath(*_) >> {
57                 def fragmentEntity = new FragmentEntity()
58                 fragmentEntity.setChildFragments([new FragmentEntity()] as Set<FragmentEntity>)
59                 return fragmentEntity
60             }
61         and: 'a data node is concurrently updated by another transaction'
62             mockFragmentRepository.save(_) >> { throw new StaleStateException("concurrent updates") }
63         when: 'attempt to update data node with submitted data nodes'
64             objectUnderTest.updateDataNodeAndDescendants('some-dataspace', 'some-anchor', new DataNodeBuilder().withXpath('/some/xpath').build())
65         then: 'concurrency exception is thrown'
66             def concurrencyException = thrown(ConcurrencyException)
67             assert concurrencyException.getDetails().contains('some-dataspace')
68             assert concurrencyException.getDetails().contains('some-anchor')
69             assert concurrencyException.getDetails().contains('/some/xpath')
70     }
71
72     def 'Handling of StaleStateException (caused by concurrent updates) during update data nodes and descendants.'() {
73         given: 'the system contains and can update one datanode'
74             def dataNode1 = mockDataNodeAndFragmentEntity('/node1', 'OK')
75         and: 'the system contains two more datanodes that throw an exception while updating'
76             def dataNode2 = mockDataNodeAndFragmentEntity('/node2', 'EXCEPTION')
77             def dataNode3 = mockDataNodeAndFragmentEntity('/node3', 'EXCEPTION')
78         and: 'the batch update will therefore also fail'
79             mockFragmentRepository.saveAll(*_) >> { throw new StaleStateException("concurrent updates") }
80         when: 'attempt batch update data nodes'
81             objectUnderTest.updateDataNodesAndDescendants('some-dataspace', 'some-anchor', [dataNode1, dataNode2, dataNode3])
82         then: 'concurrency exception is thrown'
83             def thrown = thrown(ConcurrencyException)
84             assert thrown.message == 'Concurrent Transactions'
85         and: 'it does not contain the successfull datanode'
86             assert !thrown.details.contains('/node1')
87         and: 'it contains the failed datanodes'
88             assert thrown.details.contains('/node2')
89             assert thrown.details.contains('/node3')
90     }
91
92
93     def 'Retrieving a data node with a property JSON value of #scenario'() {
94         given: 'the db has a fragment with an attribute property JSON value of #scenario'
95             mockFragmentWithJson("{\"some attribute\": ${dataString}}")
96         when: 'getting the data node represented by this fragment'
97             def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
98                     '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
99         then: 'the leaf is of the correct value and data type'
100             def attributeValue = dataNode.leaves.get('some attribute')
101             assert attributeValue == expectedValue
102             assert attributeValue.class == expectedDataClass
103         where: 'the following Data Type is passed'
104             scenario                              | dataString            || expectedValue     | expectedDataClass
105             'just numbers'                        | '15174'               || 15174             | Integer
106             'number with dot'                     | '15174.32'            || 15174.32          | Double
107             'number with 0 value after dot'       | '15174.0'             || 15174.0           | Double
108             'number with 0 value before dot'      | '0.32'                || 0.32              | Double
109             'number higher than max int'          | '2147483648'          || 2147483648        | Long
110             'just text'                           | '"Test"'              || 'Test'            | String
111             'number with exponent'                | '1.2345e5'            || 1.2345e5          | Double
112             'number higher than max int with dot' | '123456789101112.0'   || 123456789101112.0 | Double
113             'text and numbers'                    | '"String = \'1234\'"' || "String = '1234'" | String
114             'number as String'                    | '"12345"'             || '12345'           | String
115     }
116
117     def 'Retrieving a data node with invalid JSON'() {
118         given: 'a fragment with invalid JSON'
119             mockFragmentWithJson('{invalid json')
120         when: 'getting the data node represented by this fragment'
121             objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
122                     '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
123         then: 'a data validation exception is thrown'
124             thrown(DataValidationException)
125     }
126
127     def 'start session'() {
128         when: 'start session'
129             objectUnderTest.startSession()
130         then: 'the session manager method to start session is invoked'
131             1 * mockSessionManager.startSession()
132     }
133
134     def 'close session'() {
135         given: 'session ID'
136             def someSessionId = 'someSessionId'
137         when: 'close session method is called with session ID as parameter'
138             objectUnderTest.closeSession(someSessionId)
139         then: 'the session manager method to close session is invoked with parameter'
140             1 * mockSessionManager.closeSession(someSessionId, mockSessionManager.WITH_COMMIT)
141     }
142
143     def 'Lock anchor.'(){
144         when: 'lock anchor method is called with anchor entity details'
145             objectUnderTest.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L)
146         then: 'the session manager method to lock anchor is invoked with same parameters'
147             1 * mockSessionManager.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L)
148     }
149
150     def 'update data node and descendants: #scenario'(){
151         given: 'mocked responses'
152             mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/test/xpath') >> new FragmentEntity(xpath: '/test/xpath', childFragments: [])
153         when: 'replace data node tree'
154             objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', dataNodes)
155         then: 'call fragment repository save all method'
156             1 * mockFragmentRepository.saveAll({fragmentEntities -> assert fragmentEntities as List == expectedFragmentEntities})
157         where: 'the following Data Type is passed'
158             scenario                         | dataNodes                                                                          || expectedFragmentEntities
159             'empty data node list'           | []                                                                                 || []
160             'one data node in list'          | [new DataNode(xpath: '/test/xpath', leaves: ['id': 'testId'], childDataNodes: [])] || [new FragmentEntity(xpath: '/test/xpath', attributes: '{"id":"testId"}', childFragments: [])]
161     }
162
163     def 'update data nodes and descendants'() {
164         given: 'the fragment repository returns a fragment entity related to the xpath input'
165             mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/test/xpath1') >> new FragmentEntity(xpath: '/test/xpath1', childFragments: [])
166             mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/test/xpath2') >> new FragmentEntity(xpath: '/test/xpath2', childFragments: [])
167         and: 'some data nodes with descendants'
168             def dataNode1 = new DataNode(xpath: '/test/xpath1', leaves: ['id': 'testId1'], childDataNodes: [new DataNode(xpath: '/test/xpath1/child', leaves: ['id': 'childTestId1'])])
169             def dataNode2 = new DataNode(xpath: '/test/xpath2', leaves: ['id': 'testId2'], childDataNodes: [new DataNode(xpath: '/test/xpath2/child', leaves: ['id': 'childTestId2'])])
170         when: 'the fragment entities are update by the data nodes'
171             objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', [dataNode1, dataNode2])
172         then: 'call fragment repository save all method is called with the updated fragments'
173             1 * mockFragmentRepository.saveAll({fragmentEntities -> {
174                 fragmentEntities.containsAll([
175                     new FragmentEntity(xpath: '/test/xpath1', attributes: '{"id":"testId1"}', childFragments: [new FragmentEntity(xpath: '/test/xpath1/child', attributes: '{"id":"childTestId1"}', childFragments: [])]),
176                     new FragmentEntity(xpath: '/test/xpath2', attributes: '{"id":"testId2"}', childFragments: [new FragmentEntity(xpath: '/test/xpath2/child', attributes: '{"id":"childTestId2"}', childFragments: [])])
177                 ])
178                 assert fragmentEntities.size() == 2
179             }})
180     }
181
182     def mockDataNodeAndFragmentEntity(xpath, scenario) {
183         def dataNode = new DataNodeBuilder().withXpath(xpath).build()
184         def fragmentEntity = new FragmentEntity(xpath: xpath, childFragments: [])
185         mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, xpath) >> fragmentEntity
186         if ('EXCEPTION' == scenario) {
187             mockFragmentRepository.save(fragmentEntity) >> { throw new StaleStateException("concurrent updates") }
188         }
189         return dataNode
190     }
191
192     def mockFragmentWithJson(json) {
193         def anchorName = 'some anchor'
194         def anchorDataCacheEntry = new AnchorDataCacheEntry()
195         anchorDataCacheEntry.setProperty(objectUnderTest.TOP_LEVEL_MODULE_PREFIX_PROPERTY_NAME,  'some prefix')
196         mockAnchorDataCache.containsKey(anchorName) >> true
197         mockAnchorDataCache.get(anchorName) >> anchorDataCacheEntry
198         def mockAnchor = Mock(AnchorEntity)
199         mockAnchor.getId() >> 123
200         mockAnchor.getName() >> anchorName
201         mockAnchorRepository.getByDataspaceAndName(*_) >> mockAnchor
202         def mockFragmentExtract = Mock(FragmentExtract)
203         mockFragmentExtract.getId() >> 456
204         mockFragmentExtract.getAttributes() >> json
205         mockFragmentRepository.findByAnchorIdAndParentXpath(*_) >> [mockFragmentExtract]
206     }
207
208 }