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