Merge "Update Model to allow Persisting of alternateId"
[cps.git] / cps-ncmp-service / src / test / groovy / org / onap / cps / ncmp / api / impl / inventory / sync / ModuleOperationsUtilsSpec.groovy
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2022-2023 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.api.impl.inventory.sync
23
24 import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.LOCKED_MISBEHAVING
25 import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_UPGRADE
26 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL
27 import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_SYNC_FAILED
28 import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_UPGRADE_FAILED
29
30 import ch.qos.logback.classic.Level
31 import ch.qos.logback.classic.Logger
32 import ch.qos.logback.core.read.ListAppender
33 import org.slf4j.LoggerFactory
34 import org.springframework.context.annotation.AnnotationConfigApplicationContext
35 import com.fasterxml.jackson.databind.JsonNode
36 import com.fasterxml.jackson.databind.ObjectMapper
37 import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
38 import org.onap.cps.ncmp.api.impl.inventory.CmHandleQueries
39 import org.onap.cps.ncmp.api.impl.inventory.CmHandleState
40 import org.onap.cps.ncmp.api.impl.inventory.CompositeState
41 import org.onap.cps.ncmp.api.impl.inventory.CompositeStateBuilder
42 import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState
43 import org.onap.cps.spi.FetchDescendantsOption
44 import org.onap.cps.spi.model.DataNode
45 import org.onap.cps.utils.JsonObjectMapper
46 import org.springframework.http.HttpStatus
47 import org.springframework.http.ResponseEntity
48 import spock.lang.Specification
49 import java.time.OffsetDateTime
50 import java.time.format.DateTimeFormatter
51 import java.util.stream.Collectors
52
53 class ModuleOperationsUtilsSpec extends Specification{
54
55     def mockCmHandleQueries = Mock(CmHandleQueries)
56
57     def mockDmiDataOperations = Mock(DmiDataOperations)
58
59     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
60
61     def objectUnderTest = new ModuleOperationsUtils(mockCmHandleQueries, mockDmiDataOperations, jsonObjectMapper)
62
63     def static neverUpdatedBefore = '1900-01-01T00:00:00.000+0100'
64
65     def static now = OffsetDateTime.now()
66
67     def static nowAsString = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(now)
68
69     def static dataNode = new DataNode(leaves: ['id': 'cm-handle-123'])
70
71     def applicationContext = new AnnotationConfigApplicationContext()
72
73     def logger = (Logger) LoggerFactory.getLogger(ModuleOperationsUtils)
74     def loggingListAppender
75
76     void setup() {
77         logger.setLevel(Level.DEBUG)
78         loggingListAppender = new ListAppender()
79         logger.addAppender(loggingListAppender)
80         loggingListAppender.start()
81         applicationContext.refresh()
82     }
83
84     void cleanup() {
85         ((Logger) LoggerFactory.getLogger(ModuleOperationsUtils.class)).detachAndStopAllAppenders()
86         applicationContext.close()
87     }
88
89     def 'Get an advised Cm-Handle where ADVISED cm handle #scenario'() {
90         given: 'the inventory persistence service returns a collection of data nodes'
91             mockCmHandleQueries.queryCmHandlesByState(CmHandleState.ADVISED) >> dataNodeCollection
92         when: 'get advised cm handles are fetched'
93             def yangModelCmHandles = objectUnderTest.getAdvisedCmHandles()
94         then: 'the returned data node collection is the correct size'
95             yangModelCmHandles.size() == expectedDataNodeSize
96         where: 'the following scenarios are used'
97             scenario         | dataNodeCollection || expectedCallsToGetYangModelCmHandle | expectedDataNodeSize
98             'exists'         | [dataNode]         || 1                                   | 1
99             'does not exist' | []                 || 0                                   | 0
100     }
101
102     def 'Update Lock Reason, Details and Attempts where lock reason #scenario'() {
103         given: 'A locked state'
104             def compositeState = new CompositeState(lockReason: lockReason)
105         when: 'update cm handle details and attempts is called'
106             objectUnderTest.updateLockReasonDetailsAndAttempts(compositeState, MODULE_SYNC_FAILED, 'new error message')
107         then: 'the composite state lock reason and details are updated'
108             assert compositeState.lockReason.lockReasonCategory == MODULE_SYNC_FAILED
109             assert compositeState.lockReason.details.contains(expectedDetails)
110         where:
111             scenario                           | lockReason                                                                                       || expectedDetails
112             'does not exist'                   | null                                                                                             || 'Attempt #1 failed: new error message'
113             'exists'                           | CompositeState.LockReason.builder().details("Attempt #2 failed: some error message").build()     || 'Attempt #3 failed: new error message'
114     }
115
116     def 'Update lock reason details that contains #scenario'() {
117         given: 'A locked state'
118             def compositeState = new CompositeStateBuilder().withCmHandleState(CmHandleState.LOCKED)
119                 .withLockReason(MODULE_UPGRADE, "Upgrade to ModuleSetTag: " + moduleSetTag).build()
120         when: 'update cm handle details'
121             objectUnderTest.updateLockReasonDetailsAndAttempts(compositeState, MODULE_UPGRADE_FAILED, 'new error message')
122         then: 'the composite state lock reason and details are updated'
123             assert compositeState.lockReason.lockReasonCategory == MODULE_UPGRADE_FAILED
124             assert compositeState.lockReason.details.contains("Upgrade to ModuleSetTag: " + expectedDetails)
125         where:
126             scenario               | moduleSetTag       || expectedDetails
127             'a module set tag'     | 'someModuleSetTag' || 'someModuleSetTag'
128             'empty module set tag' | ''                 || 'not-specified'
129     }
130
131     def 'Get all locked Cm-Handle where Lock Reason is MODULE_SYNC_FAILED cm handle #scenario'() {
132         given: 'the cps (persistence service) returns a collection of data nodes'
133             mockCmHandleQueries.queryCmHandleAncestorsByCpsPath(
134                     '//lock-reason[@reason="MODULE_SYNC_FAILED" or @reason="MODULE_UPGRADE"]',
135                 FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode]
136         when: 'get locked Misbehaving cm handle is called'
137             def result = objectUnderTest.getCmHandlesThatFailedModelSyncOrUpgrade()
138         then: 'the returned cm handle collection is the correct size'
139             result.size() == 1
140         and: 'the correct cm handle is returned'
141             result[0].id == 'cm-handle-123'
142     }
143
144     def 'Retry Locked Cm-Handle where the last update time is #scenario'() {
145         given: 'Last update was #lastUpdateMinutesAgo minutes ago (-1 means never)'
146             def lastUpdatedTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(now.minusMinutes(lastUpdateMinutesAgo))
147             if (lastUpdateMinutesAgo < 0 ) {
148                 lastUpdatedTime = neverUpdatedBefore
149             }
150         when: 'checking to see if cm handle is ready for retry'
151          def result = objectUnderTest.needsModuleSyncRetryOrUpgrade(new CompositeStateBuilder()
152                 .withLockReason(MODULE_SYNC_FAILED, lockDetails)
153                 .withLastUpdatedTime(lastUpdatedTime).build())
154         then: 'retry is only attempted when expected'
155             assert result == retryExpected
156         and: 'logs contain related information'
157             def logs = loggingListAppender.list.toString()
158             assert logs.contains(logReason)
159         where: 'the following parameters are used'
160             scenario                                    | lastUpdateMinutesAgo | lockDetails                     | logReason                               || retryExpected
161             'never attempted before'                    | -1                   | 'Fist attempt:'                 | 'First Attempt:'                        || true
162             '1st attempt, last attempt > 2 minute ago'  | 3                    | 'Attempt #1 failed: some error' | 'Retry due now'                         || true
163             '2nd attempt, last attempt < 4 minutes ago' | 1                    | 'Attempt #2 failed: some error' | 'Time until next attempt is 3 minutes:' || false
164             '2nd attempt, last attempt > 4 minutes ago' | 5                    | 'Attempt #2 failed: some error' | 'Retry due now'                         || true
165     }
166
167     def 'Retry Locked Cm-Handle with lock reasons (category) #lockReasonCategory'() {
168         when: 'checking to see if cm handle is ready for retry'
169             def result = objectUnderTest.needsModuleSyncRetryOrUpgrade(new CompositeStateBuilder()
170                 .withLockReason(lockReasonCategory, 'some details')
171                 .withLastUpdatedTime(nowAsString).build())
172         then: 'verify retry attempts'
173             assert !result
174         and: 'logs contain related information'
175             def logs = loggingListAppender.list.toString()
176             assert logs.contains(logReason)
177         where: 'the following lock reasons occurred'
178             scenario             | lockReasonCategory    || logReason
179             'module upgrade'     | MODULE_UPGRADE_FAILED || 'First Attempt:'
180             'module sync failed' | MODULE_SYNC_FAILED    || 'First Attempt:'
181             'lock misbehaving'   | LOCKED_MISBEHAVING    || 'Locked for other reason'
182     }
183
184     def 'Get a Cm-Handle where #scenario'() {
185         given: 'the inventory persistence service returns a collection of data nodes'
186             mockCmHandleQueries.queryCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED) >> unSynchronizedDataNodes
187             mockCmHandleQueries.cmHandleHasState('cm-handle-123', CmHandleState.READY) >> cmHandleHasState
188         when: 'get advised cm handles are fetched'
189             def yangModelCollection = objectUnderTest.getUnsynchronizedReadyCmHandles()
190         then: 'the returned data node collection is the correct size'
191             yangModelCollection.size() == expectedDataNodeSize
192         and: 'the result contains the correct data'
193             yangModelCollection.stream().map(yangModel -> yangModel.id).collect(Collectors.toSet()) == expectedYangModelCollectionIds
194         where: 'the following scenarios are used'
195             scenario                                   | unSynchronizedDataNodes | cmHandleHasState || expectedDataNodeSize | expectedYangModelCollectionIds
196             'a Cm-Handle unsynchronized and ready'     | [dataNode]              | true             || 1                    | ['cm-handle-123'] as Set
197             'a Cm-Handle unsynchronized but not ready' | [dataNode]              | false            || 0                    | [] as Set
198             'all Cm-Handle synchronized'               | []                      | false            || 0                    | [] as Set
199     }
200
201     def 'Get resource data through DMI Operations #scenario'() {
202         given: 'the inventory persistence service returns a collection of data nodes'
203             def jsonString = '{"stores:bookstore":{"categories":[{"code":"01"}]}}'
204             JsonNode jsonNode = jsonObjectMapper.convertToJsonNode(jsonString);
205             def responseEntity = new ResponseEntity<>(jsonNode, HttpStatus.OK)
206             mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_OPERATIONAL.datastoreName, 'cm-handle-123', _) >> responseEntity
207         when: 'get resource data is called'
208             def result = objectUnderTest.getResourceData('cm-handle-123')
209         then: 'the returned data is correct'
210             result == jsonString
211     }
212
213     def 'Extract module set tag and number of attempt when lock reason contains #scenario'() {
214         expect: 'lock reason details are extracted correctly'
215             def result = objectUnderTest.getLockedCompositeStateDetails(new CompositeStateBuilder().withLockReason(MODULE_UPGRADE, lockReasonDetails).build().lockReason)
216         and: 'the result contains the correct moduleSetTag'
217             assert result['moduleSetTag'] == expectedModuleSetTag
218         and: 'the result contains the correct number of attempts'
219             assert result['attempt'] == expectedNumberOfAttempts
220         where: 'the following scenarios are used'
221             scenario                                     | lockReasonDetails                                                           || expectedModuleSetTag | expectedNumberOfAttempts
222             'module set tag only'                        | 'Upgrade to ModuleSetTag: targetModuleSetTag'                               || 'targetModuleSetTag' | null
223             'number of attempts only'                    | 'Attempt #1 failed: some error'                                             || null                 | '1'
224             'number of attempts and module set tag both' | 'Upgrade to ModuleSetTag: targetModuleSetTag Attempt #1 failed: some error' || 'targetModuleSetTag' | '1'
225     }
226 }