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.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;
43 class CpsDataPersistenceServiceSpec extends Specification {
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>)
52 def objectUnderTest = new CpsDataPersistenceServiceImpl(
53 mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper, mockSessionManager, mockAnchorDataCache)
56 def NEW_RESOURCE_CONTENT = 'module stores {\n' +
57 ' yang-version 1.1;\n' +
58 ' namespace "org:onap:ccsdk:sample";\n' +
60 ' prefix book-store;\n' +
62 ' revision "2020-09-15" {\n' +
64 ' "Sample Model";\n' +
69 def yangResourceSet = [new YangResourceEntity(moduleName: 'moduleName', content: NEW_RESOURCE_CONTENT,
70 fileName: 'sampleYangResource'
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>)
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')
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')
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 )))
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
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')
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)
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()
160 def 'close session'() {
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)
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)
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: [])]
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: [])])
204 assert fragmentEntities.size() == 2
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") }