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
10 * http://www.apache.org/licenses/LICENSE-2.0
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=========================================================
20 package org.onap.cps.spi.impl
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;
42 class CpsDataPersistenceServiceSpec extends Specification {
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>)
51 def objectUnderTest = new CpsDataPersistenceServiceImpl(
52 mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper, mockSessionManager, mockAnchorDataCache)
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>)
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')
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')
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
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)
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()
134 def 'close session'() {
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)
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)
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: [])]
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: [])])
178 assert fragmentEntities.size() == 2
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") }
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]