Merge "Add memory usage to integration tests [UPDATED]"
[cps.git] / cps-ncmp-service / src / test / groovy / org / onap / cps / ncmp / api / 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.inventory.sync
23
24 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL
25 import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_SYNC_FAILED
26 import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_UPGRADE
27 import static org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory.MODULE_UPGRADE_FAILED
28
29 import ch.qos.logback.classic.Level
30 import ch.qos.logback.classic.Logger
31 import ch.qos.logback.core.read.ListAppender
32 import org.onap.cps.ncmp.api.impl.inventory.sync.SyncUtils
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 SyncUtilsSpec 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 SyncUtils(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(SyncUtils)
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(SyncUtils.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 == 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 'Get all locked Cm-Handle where Lock Reason is MODULE_SYNC_FAILED cm handle #scenario'() {
117         given: 'the cps (persistence service) returns a collection of data nodes'
118             mockCmHandleQueries.queryCmHandleAncestorsByCpsPath(
119                     '//lock-reason[@reason="MODULE_SYNC_FAILED" or @reason="MODULE_UPGRADE"]',
120                 FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode]
121         when: 'get locked Misbehaving cm handle is called'
122             def result = objectUnderTest.getCmHandlesThatFailedModelSyncOrUpgrade()
123         then: 'the returned cm handle collection is the correct size'
124             result.size() == 1
125         and: 'the correct cm handle is returned'
126             result[0].id == 'cm-handle-123'
127     }
128
129     def 'Retry Locked Cm-Handle where the last update time is #scenario'() {
130         given: 'Last update was #lastUpdateMinutesAgo minutes ago (-1 means never)'
131             def lastUpdatedTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(now.minusMinutes(lastUpdateMinutesAgo))
132             if (lastUpdateMinutesAgo < 0 ) {
133                 lastUpdatedTime = neverUpdatedBefore
134             }
135         when: 'checking to see if cm handle is ready for retry'
136          def result = objectUnderTest.needsModuleSyncRetryOrUpgrade(new CompositeStateBuilder()
137                 .withLockReason(MODULE_SYNC_FAILED, lockDetails)
138                 .withLastUpdatedTime(lastUpdatedTime).build())
139         then: 'retry is only attempted when expected'
140             assert result == retryExpected
141         and: 'logs contain related information'
142             def logs = loggingListAppender.list.toString()
143             assert logs.contains(logReason)
144         where: 'the following parameters are used'
145             scenario                                    | lastUpdateMinutesAgo | lockDetails          | logReason                               || retryExpected
146             'never attempted before'                    | -1                   | 'fist attempt:'      | 'First Attempt:'                        || true
147             '1st attempt, last attempt > 2 minute ago'  | 3                    | 'Attempt #1 failed:' | 'Retry due now'                         || true
148             '2nd attempt, last attempt < 4 minutes ago' | 1                    | 'Attempt #2 failed:' | 'Time until next attempt is 3 minutes:' || false
149             '2nd attempt, last attempt > 4 minutes ago' | 5                    | 'Attempt #2 failed:' | 'Retry due now'                         || true
150     }
151
152     def 'Retry Locked Cm-Handle with other lock reasons (category) #lockReasonCategory'() {
153         when: 'checking to see if cm handle is ready for retry'
154         def result = objectUnderTest.needsModuleSyncRetryOrUpgrade(new CompositeStateBuilder()
155                 .withLockReason(lockReasonCategory, 'some details')
156                 .withLastUpdatedTime(nowAsString).build())
157         then: 'verify retry attempts'
158         assert result == retryAttempt
159         and: 'logs contain related information'
160         def logs = loggingListAppender.list.toString()
161         assert logs.contains(logReason)
162         where: 'the following lock reasons occurred'
163         scenario             | lockReasonCategory || logReason                    | retryAttempt
164         'module upgrade'     | MODULE_UPGRADE     || 'Locked for module upgrade.' | true
165         'module sync failed' | MODULE_SYNC_FAILED || 'First Attempt:'             | false
166     }
167
168     def 'Get a Cm-Handle where #scenario'() {
169         given: 'the inventory persistence service returns a collection of data nodes'
170             mockCmHandleQueries.queryCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED) >> unSynchronizedDataNodes
171             mockCmHandleQueries.cmHandleHasState('cm-handle-123', CmHandleState.READY) >> cmHandleHasState
172         when: 'get advised cm handles are fetched'
173             def yangModelCollection = objectUnderTest.getUnsynchronizedReadyCmHandles()
174         then: 'the returned data node collection is the correct size'
175             yangModelCollection.size() == expectedDataNodeSize
176         and: 'the result contains the correct data'
177             yangModelCollection.stream().map(yangModel -> yangModel.id).collect(Collectors.toSet()) == expectedYangModelCollectionIds
178         where: 'the following scenarios are used'
179             scenario                                   | unSynchronizedDataNodes | cmHandleHasState || expectedDataNodeSize | expectedYangModelCollectionIds
180             'a Cm-Handle unsynchronized and ready'     | [dataNode]              | true             || 1                    | ['cm-handle-123'] as Set
181             'a Cm-Handle unsynchronized but not ready' | [dataNode]              | false            || 0                    | [] as Set
182             'all Cm-Handle synchronized'               | []                      | false            || 0                    | [] as Set
183     }
184
185     def 'Get resource data through DMI Operations #scenario'() {
186         given: 'the inventory persistence service returns a collection of data nodes'
187             def jsonString = '{"stores:bookstore":{"categories":[{"code":"01"}]}}'
188             JsonNode jsonNode = jsonObjectMapper.convertToJsonNode(jsonString);
189             def responseEntity = new ResponseEntity<>(jsonNode, HttpStatus.OK)
190             mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_OPERATIONAL.datastoreName, 'cm-handle-123', _) >> responseEntity
191         when: 'get resource data is called'
192             def result = objectUnderTest.getResourceData('cm-handle-123')
193         then: 'the returned data is correct'
194             result == jsonString
195     }
196 }