de5f7e2fbe1e43d5ded42be92bd64fc123f0c16c
[cps.git] /
1 /*
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
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  *
18  *  SPDX-License-Identifier: Apache-2.0
19  *  ============LICENSE_END=========================================================
20  */
21
22 package org.onap.cps.ncmp.impl.inventory.sync
23
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
44
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
48
49 class ModuleOperationsUtilsSpec extends Specification{
50
51     def mockCmHandleQueries = Mock(CmHandleQueryService)
52
53     def mockDmiDataOperations = Mock(DmiDataOperations)
54
55     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
56
57     def objectUnderTest = new ModuleOperationsUtils(mockCmHandleQueries, mockDmiDataOperations, jsonObjectMapper)
58
59     def static dataNode = new DataNode(leaves: ['id': 'cm-handle-123'])
60
61     def applicationContext = new AnnotationConfigApplicationContext()
62
63     def logger = (Logger) LoggerFactory.getLogger(ModuleOperationsUtils)
64
65     def loggingListAppender
66
67     void setup() {
68         logger.setLevel(Level.DEBUG)
69         loggingListAppender = new ListAppender()
70         logger.addAppender(loggingListAppender)
71         loggingListAppender.start()
72         applicationContext.refresh()
73     }
74
75     void cleanup() {
76         ((Logger) LoggerFactory.getLogger(ModuleOperationsUtils.class)).detachAndStopAllAppenders()
77         applicationContext.close()
78     }
79
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
91     }
92
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)
101         where:
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'
105     }
106
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)
116         where:
117             scenario               | moduleSetTag       || expectedDetails
118             'a module set tag'     | 'someModuleSetTag' || 'Upgrade to ModuleSetTag: someModuleSetTag'
119             'empty module set tag' | ''                 || 'Attempt'
120     }
121
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'
132     }
133
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
149     }
150
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
162         where:
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
167     }
168
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'
181     }
182 }