2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2022-2024 Nordix Foundation
4 * Modifications Copyright (C) 2022 Bell Canada
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.
18 * SPDX-License-Identifier: Apache-2.0
19 * ============LICENSE_END=========================================================
22 package org.onap.cps.ncmp.impl.inventory.sync
24 import ch.qos.logback.classic.Level
25 import ch.qos.logback.classic.Logger
26 import ch.qos.logback.core.read.ListAppender
27 import com.fasterxml.jackson.databind.JsonNode
28 import com.fasterxml.jackson.databind.ObjectMapper
29 import org.onap.cps.ncmp.api.inventory.models.CompositeState
30 import org.onap.cps.ncmp.api.inventory.models.CompositeStateBuilder
31 import org.onap.cps.ncmp.impl.data.DmiDataOperations
32 import org.onap.cps.ncmp.impl.inventory.CmHandleQueryService
33 import org.onap.cps.ncmp.impl.inventory.DataStoreSyncState
34 import org.onap.cps.ncmp.impl.inventory.models.CmHandleState
35 import org.onap.cps.api.parameters.FetchDescendantsOption
36 import org.onap.cps.api.model.DataNode
37 import org.onap.cps.utils.JsonObjectMapper
38 import org.slf4j.LoggerFactory
39 import org.springframework.context.annotation.AnnotationConfigApplicationContext
40 import org.springframework.http.HttpStatus
41 import org.springframework.http.ResponseEntity
42 import spock.lang.Specification
43 import java.util.stream.Collectors
45 import static org.onap.cps.ncmp.impl.inventory.models.LockReasonCategory.MODULE_SYNC_FAILED
46 import static org.onap.cps.ncmp.impl.inventory.models.LockReasonCategory.MODULE_UPGRADE
47 import static org.onap.cps.ncmp.impl.inventory.models.LockReasonCategory.MODULE_UPGRADE_FAILED
49 class ModuleOperationsUtilsSpec extends Specification{
51 def mockCmHandleQueries = Mock(CmHandleQueryService)
53 def mockDmiDataOperations = Mock(DmiDataOperations)
55 def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
57 def objectUnderTest = new ModuleOperationsUtils(mockCmHandleQueries, mockDmiDataOperations, jsonObjectMapper)
59 def static dataNode = new DataNode(leaves: ['id': 'cm-handle-123'])
61 def applicationContext = new AnnotationConfigApplicationContext()
63 def logger = (Logger) LoggerFactory.getLogger(ModuleOperationsUtils)
65 def loggingListAppender
68 logger.setLevel(Level.DEBUG)
69 loggingListAppender = new ListAppender()
70 logger.addAppender(loggingListAppender)
71 loggingListAppender.start()
72 applicationContext.refresh()
76 ((Logger) LoggerFactory.getLogger(ModuleOperationsUtils.class)).detachAndStopAllAppenders()
77 applicationContext.close()
80 def 'Get an advised Cm-Handle where ADVISED cm handle #scenario'() {
81 given: 'the inventory persistence service returns a collection of data nodes'
82 mockCmHandleQueries.queryCmHandlesByState(CmHandleState.ADVISED) >> dataNodeCollection
83 when: 'get advised cm handles are fetched'
84 def yangModelCmHandles = objectUnderTest.getAdvisedCmHandles()
85 then: 'the returned data node collection is the correct size'
86 yangModelCmHandles.size() == expectedDataNodeSize
87 where: 'the following scenarios are used'
88 scenario | dataNodeCollection || expectedCallsToGetYangModelCmHandle | expectedDataNodeSize
89 'exists' | [dataNode] || 1 | 1
90 'does not exist' | [] || 0 | 0
93 def 'Update Lock Reason, Details and Attempts where lock reason #scenario'() {
94 given: 'A locked state'
95 def compositeState = new CompositeState(lockReason: lockReason)
96 when: 'update cm handle details and attempts is called'
97 objectUnderTest.updateLockReasonWithAttempts(compositeState, MODULE_SYNC_FAILED, 'new error message')
98 then: 'the composite state lock reason and details are updated'
99 assert compositeState.lockReason.lockReasonCategory == MODULE_SYNC_FAILED
100 assert compositeState.lockReason.details.contains(expectedDetails)
102 scenario | lockReason || expectedDetails
103 'does not exist' | null || 'Attempt #1 failed: new error message'
104 'exists' | CompositeState.LockReason.builder().details("Attempt #2 failed: some error message").build() || 'Attempt #3 failed: new error message'
107 def 'Update lock reason details that contains #scenario'() {
108 given: 'A locked state'
109 def compositeState = new CompositeStateBuilder().withCmHandleState(CmHandleState.LOCKED)
110 .withLockReason(MODULE_UPGRADE, "Upgrade to ModuleSetTag: " + moduleSetTag).build()
111 when: 'update cm handle details'
112 objectUnderTest.updateLockReasonWithAttempts(compositeState, MODULE_UPGRADE_FAILED, 'new error message')
113 then: 'the composite state lock reason and details are updated'
114 assert compositeState.lockReason.lockReasonCategory == MODULE_UPGRADE_FAILED
115 assert compositeState.lockReason.details.contains(expectedDetails)
117 scenario | moduleSetTag || expectedDetails
118 'a module set tag' | 'someModuleSetTag' || 'Upgrade to ModuleSetTag: someModuleSetTag'
119 'empty module set tag' | '' || 'Attempt'
122 def 'Get all locked cm-Handles where lock reasons are model sync failed or upgrade'() {
123 given: 'the cps (persistence service) returns a collection of data nodes'
124 mockCmHandleQueries.queryCmHandleAncestorsByCpsPath(ModuleOperationsUtils.CPS_PATH_CM_HANDLES_MODEL_SYNC_FAILED_OR_UPGRADE,
125 FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode]
126 when: 'get locked Misbehaving cm handle is called'
127 def result = objectUnderTest.getCmHandlesThatFailedModelSyncOrUpgrade()
128 then: 'the returned cm handle collection is the correct size'
129 assert result.size() == 1
130 and: 'the correct cm handle is returned'
131 assert result[0].id == 'cm-handle-123'
134 def 'Get a Cm-Handle where #scenario'() {
135 given: 'the inventory persistence service returns a collection of data nodes'
136 mockCmHandleQueries.queryCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED) >> unSynchronizedDataNodes
137 mockCmHandleQueries.cmHandleHasState('cm-handle-123', CmHandleState.READY) >> cmHandleHasState
138 when: 'get advised cm handles are fetched'
139 def yangModelCollection = objectUnderTest.getUnsynchronizedReadyCmHandles()
140 then: 'the returned data node collection is the correct size'
141 yangModelCollection.size() == expectedDataNodeSize
142 and: 'the result contains the correct data'
143 yangModelCollection.stream().map(yangModel -> yangModel.id).collect(Collectors.toSet()) == expectedYangModelCollectionIds
144 where: 'the following scenarios are used'
145 scenario | unSynchronizedDataNodes | cmHandleHasState || expectedDataNodeSize | expectedYangModelCollectionIds
146 'a Cm-Handle unsynchronized and ready' | [dataNode] | true || 1 | ['cm-handle-123'] as Set
147 'a Cm-Handle unsynchronized but not ready' | [dataNode] | false || 0 | [] as Set
148 'all Cm-Handle synchronized' | [] | false || 0 | [] as Set
151 def 'Retrieve resource data from DMI operations for #scenario'() {
152 given: 'a JSON string representing the resource data'
153 def jsonString = '{"stores:bookstore":{"categories":[{"code":"01"}]}}'
154 JsonNode jsonNode = jsonObjectMapper.convertToJsonNode(jsonString)
155 and: 'DMI operations are mocked to return a response based on the scenario'
156 def responseEntity = new ResponseEntity<>(statusCode == HttpStatus.OK ? jsonNode : null, statusCode)
157 mockDmiDataOperations.getAllResourceDataFromDmi('cm-handle-123', _) >> responseEntity
158 when: 'get resource data is called'
159 def result = objectUnderTest.getResourceData('cm-handle-123')
160 then: 'the returned data matches the expected result'
161 assert result == expectedResult
163 scenario | statusCode | expectedResult
164 'successful response' | HttpStatus.OK | '{"stores:bookstore":{"categories":[{"code":"01"}]}}'
165 'response with not found status' | HttpStatus.NOT_FOUND | null
166 'response with internal server error' | HttpStatus.INTERNAL_SERVER_ERROR | null
169 def 'Extract module set tag and number of attempt when lock reason contains #scenario'() {
170 expect: 'lock reason details are extracted correctly'
171 def result = objectUnderTest.getLockedCompositeStateDetails(new CompositeStateBuilder().withLockReason(MODULE_UPGRADE, lockReasonDetails).build().lockReason)
172 and: 'the result contains the correct moduleSetTag'
173 assert result['moduleSetTag'] == expectedModuleSetTag
174 and: 'the result contains the correct number of attempts'
175 assert result['attempt'] == expectedNumberOfAttempts
176 where: 'the following scenarios are used'
177 scenario | lockReasonDetails || expectedModuleSetTag | expectedNumberOfAttempts
178 'module set tag only' | 'Upgrade to ModuleSetTag: targetModuleSetTag' || 'targetModuleSetTag' | null
179 'number of attempts only' | 'Attempt #1 failed: some error' || null | '1'
180 'number of attempts and module set tag both' | 'Upgrade to ModuleSetTag: targetModuleSetTag Attempt #1 failed: some error' || 'targetModuleSetTag' | '1'