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.DataspaceEntity
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.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 org.onap.cps.yang.YangTextSchemaSourceSet
39 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
40 import org.opendaylight.yangtools.yang.model.api.SchemaContext
41 import spock.lang.Shared
42 import spock.lang.Specification
44 class CpsDataPersistenceServiceSpec extends Specification {
46 def mockDataspaceRepository = Mock(DataspaceRepository)
47 def mockAnchorRepository = Mock(AnchorRepository)
48 def mockFragmentRepository = Mock(FragmentRepository)
49 def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
50 def mockSessionManager = Mock(SessionManager)
52 def objectUnderTest = new CpsDataPersistenceServiceImpl(
53 mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper,mockSessionManager)
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 name: 'sampleYangResource'
74 def 'Handling of StaleStateException (caused by concurrent updates) during data node tree update.'() {
76 def parentXpath = '/parent-01'
77 def myDataspaceName = 'my-dataspace'
78 def myAnchorName = 'my-anchor'
80 given: 'data node object'
81 def submittedDataNode = new DataNodeBuilder()
82 .withXpath(parentXpath)
83 .withLeaves(['leaf-name': 'leaf-value'])
85 and: 'fragment to be updated'
86 mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
87 def fragmentEntity = new FragmentEntity()
88 fragmentEntity.setXpath(parentXpath)
89 fragmentEntity.setChildFragments(Collections.emptySet())
92 and: 'data node is concurrently updated by another transaction'
93 mockFragmentRepository.save(_) >> { throw new StaleStateException("concurrent updates") }
95 when: 'attempt to update data node'
96 objectUnderTest.replaceDataNodeTree(myDataspaceName, myAnchorName, submittedDataNode)
98 then: 'concurrency exception is thrown'
99 def concurrencyException = thrown(ConcurrencyException)
100 assert concurrencyException.getDetails().contains(myDataspaceName)
101 assert concurrencyException.getDetails().contains(myAnchorName)
102 assert concurrencyException.getDetails().contains(parentXpath)
105 def 'Retrieving a data node with a property JSON value of #scenario'() {
106 given: 'a fragment with a property JSON value of #scenario'
107 mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
108 new FragmentEntity(childFragments: Collections.emptySet(),
109 attributes: "{\"some attribute\": ${dataString}}",
110 anchor: new AnchorEntity(schemaSet: new SchemaSetEntity(yangResources: yangResourceSet )))
112 when: 'getting the data node represented by this fragment'
113 def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
114 '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
115 then: 'the leaf is of the correct value and data type'
116 def attributeValue = dataNode.leaves.get('some attribute')
117 assert attributeValue == expectedValue
118 assert attributeValue.class == expectedDataClass
119 where: 'the following Data Type is passed'
120 scenario | dataString || expectedValue | expectedDataClass
121 'just numbers' | '15174' || 15174 | Integer
122 'number with dot' | '15174.32' || 15174.32 | Double
123 'number with 0 value after dot' | '15174.0' || 15174.0 | Double
124 'number with 0 value before dot' | '0.32' || 0.32 | Double
125 'number higher than max int' | '2147483648' || 2147483648 | Long
126 'just text' | '"Test"' || 'Test' | String
127 'number with exponent' | '1.2345e5' || 1.2345e5 | Double
128 'number higher than max int with dot' | '123456789101112.0' || 123456789101112.0 | Double
129 'text and numbers' | '"String = \'1234\'"' || "String = '1234'" | String
130 'number as String' | '"12345"' || '12345' | String
133 def 'Retrieving a data node with invalid JSON'() {
134 given: 'a fragment with invalid JSON'
135 mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, _) >> {
136 new FragmentEntity(childFragments: Collections.emptySet(), attributes: '{invalid json')
138 when: 'getting the data node represented by this fragment'
139 def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
140 '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
141 then: 'a data validation exception is thrown'
142 thrown(DataValidationException)
145 def 'start session'() {
146 when: 'start session'
147 objectUnderTest.startSession()
148 then: 'the session manager method to start session is invoked'
149 1 * mockSessionManager.startSession()
152 def 'close session'() {
154 def someSessionId = 'someSessionId'
155 when: 'close session method is called with session ID as parameter'
156 objectUnderTest.closeSession(someSessionId)
157 then: 'the session manager method to close session is invoked with parameter'
158 1 * mockSessionManager.closeSession(someSessionId, mockSessionManager.WITH_COMMIT)
161 def 'Lock anchor.'(){
162 when: 'lock anchor method is called with anchor entity details'
163 objectUnderTest.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L)
164 then: 'the session manager method to lock anchor is invoked with same parameters'
165 1 * mockSessionManager.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L)