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.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
41 class CpsDataPersistenceServiceSpec extends Specification {
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)
49 def objectUnderTest = new CpsDataPersistenceServiceImpl(
50 mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper,mockSessionManager)
53 def NEW_RESOURCE_CONTENT = 'module stores {\n' +
54 ' yang-version 1.1;\n' +
55 ' namespace "org:onap:ccsdk:sample";\n' +
57 ' prefix book-store;\n' +
59 ' revision "2020-09-15" {\n' +
61 ' "Sample Model";\n' +
66 def yangResourceSet = [new YangResourceEntity(moduleName: 'moduleName', content: NEW_RESOURCE_CONTENT,
67 fileName: 'sampleYangResource'
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>)
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')
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')
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 )))
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
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')
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)
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()
149 def 'close session'() {
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)
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)
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: [])]
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: [])])
193 assert fragmentEntities.size() == 2