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