Add CSIT test: Delete CM Handle
[cps.git] / cps-ncmp-service / src / test / groovy / org / onap / cps / ncmp / api / impl / inventory / sync / ModuleSyncTasksSpec.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
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_FAILED
27
28 import ch.qos.logback.classic.Level
29 import ch.qos.logback.classic.Logger
30 import ch.qos.logback.classic.spi.ILoggingEvent
31 import ch.qos.logback.core.read.ListAppender
32 import com.hazelcast.config.Config
33 import com.hazelcast.instance.impl.HazelcastInstanceFactory
34 import com.hazelcast.map.IMap
35 import org.junit.jupiter.api.AfterEach
36 import org.junit.jupiter.api.BeforeEach
37 import org.onap.cps.ncmp.api.impl.events.lcm.LcmEventsCmHandleStateHandler
38 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
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.InventoryPersistence
43 import org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory
44 import org.onap.cps.spi.model.DataNode
45 import org.slf4j.LoggerFactory
46 import spock.lang.Specification
47 import java.util.concurrent.atomic.AtomicInteger
48
49 class ModuleSyncTasksSpec extends Specification {
50
51     def logger = Spy(ListAppender<ILoggingEvent>)
52
53     @BeforeEach
54     void setup() {
55         ((Logger) LoggerFactory.getLogger(ModuleSyncTasks.class)).addAppender(logger);
56         logger.start();
57     }
58
59     @AfterEach
60     void teardown() {
61         ((Logger) LoggerFactory.getLogger(ModuleSyncTasks.class)).detachAndStopAllAppenders();
62     }
63
64     def mockInventoryPersistence = Mock(InventoryPersistence)
65
66     def mockSyncUtils = Mock(ModuleOperationsUtils)
67
68     def mockModuleSyncService = Mock(ModuleSyncService)
69
70     def mockLcmEventsCmHandleStateHandler = Mock(LcmEventsCmHandleStateHandler)
71
72     IMap<String, Object> moduleSyncStartedOnCmHandles = HazelcastInstanceFactory
73             .getOrCreateHazelcastInstance(new Config('hazelcastInstanceName'))
74             .getMap('mapInstanceName')
75
76     def batchCount = new AtomicInteger(5)
77
78     def objectUnderTest = new ModuleSyncTasks(mockInventoryPersistence, mockSyncUtils, mockModuleSyncService,
79             mockLcmEventsCmHandleStateHandler, moduleSyncStartedOnCmHandles)
80
81     def 'Module Sync ADVISED cm handles.'() {
82         given: 'cm handles in an ADVISED state'
83             def cmHandle1 = cmHandleAsDataNodeByIdAndState('cm-handle-1', CmHandleState.ADVISED)
84             def cmHandle2 = cmHandleAsDataNodeByIdAndState('cm-handle-2', CmHandleState.ADVISED)
85         and: 'the inventory persistence cm handle returns a ADVISED state for the any handle'
86             mockInventoryPersistence.getCmHandleState(_) >> new CompositeState(cmHandleState: CmHandleState.ADVISED)
87         when: 'module sync poll is executed'
88             objectUnderTest.performModuleSync([cmHandle1, cmHandle2], batchCount)
89         then: 'module sync service deletes schemas set of each cm handle if it already exists'
90             1 * mockModuleSyncService.deleteSchemaSetIfExists('cm-handle-1')
91             1 * mockModuleSyncService.deleteSchemaSetIfExists('cm-handle-2')
92         and: 'module sync service is invoked for each cm handle'
93             1 * mockModuleSyncService.syncAndCreateOrUpgradeSchemaSetAndAnchor(_) >> { args -> assertYamgModelCmHandleArgument(args, 'cm-handle-1') }
94             1 * mockModuleSyncService.syncAndCreateOrUpgradeSchemaSetAndAnchor(_) >> { args -> assertYamgModelCmHandleArgument(args, 'cm-handle-2') }
95         and: 'the state handler is called for the both cm handles'
96             1 * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(_) >> { args ->
97                 assertBatch(args, ['cm-handle-1', 'cm-handle-2'], CmHandleState.READY)
98             }
99         and: 'batch count is decremented by one'
100             assert batchCount.get() == 4
101     }
102
103     def 'Module Sync ADVISED cm handle with failure during sync.'() {
104         given: 'a cm handle in an ADVISED state'
105             def cmHandle = cmHandleAsDataNodeByIdAndState('cm-handle', CmHandleState.ADVISED)
106         and: 'the inventory persistence cm handle returns a ADVISED state for the cm handle'
107             def cmHandleState = new CompositeState(cmHandleState: CmHandleState.ADVISED)
108             1 * mockInventoryPersistence.getCmHandleState('cm-handle') >> cmHandleState
109         and: 'module sync service attempts to sync the cm handle and throws an exception'
110             1 * mockModuleSyncService.syncAndCreateOrUpgradeSchemaSetAndAnchor(*_) >> { throw new Exception('some exception') }
111         when: 'module sync is executed'
112             objectUnderTest.performModuleSync([cmHandle], batchCount)
113         then: 'update lock reason, details and attempts is invoked'
114             1 * mockSyncUtils.updateLockReasonDetailsAndAttempts(cmHandleState, MODULE_SYNC_FAILED, 'some exception')
115         and: 'the state handler is called to update the state to LOCKED'
116             1 * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(_) >> { args ->
117                 assertBatch(args, ['cm-handle'], CmHandleState.LOCKED)
118             }
119         and: 'batch count is decremented by one'
120             assert batchCount.get() == 4
121     }
122
123     def 'Failed cm handle during #scenario.'() {
124         given: 'a cm handle in LOCKED state'
125             def cmHandle = cmHandleAsDataNodeByIdAndState('cm-handle', CmHandleState.LOCKED)
126         and: 'the inventory persistence cm handle returns a LOCKED state with reason for the cm handle'
127             def expectedCmHandleState = new CompositeState(cmHandleState: cmHandleState, lockReason: CompositeState
128                 .LockReason.builder().lockReasonCategory(lockReasonCategory).details(lockReasonDetails).build())
129             1 * mockInventoryPersistence.getCmHandleState('cm-handle') >> expectedCmHandleState
130         and: 'module sync service attempts to sync/upgrade the cm handle and throws an exception'
131             1 * mockModuleSyncService.syncAndCreateOrUpgradeSchemaSetAndAnchor(*_) >> { throw new Exception('some exception') }
132         when: 'module sync is executed'
133             objectUnderTest.performModuleSync([cmHandle], batchCount)
134         then: 'update lock reason, details and attempts is invoked'
135             1 * mockSyncUtils.updateLockReasonDetailsAndAttempts(expectedCmHandleState, expectedLockReasonCategory, 'some exception')
136         where:
137             scenario         | cmHandleState        | lockReasonCategory    | lockReasonDetails                              || expectedLockReasonCategory
138             'module upgrade' | CmHandleState.LOCKED | MODULE_UPGRADE_FAILED | 'Upgrade to ModuleSetTag: some-module-set-tag' || MODULE_UPGRADE_FAILED
139             'module sync'    | CmHandleState.LOCKED | MODULE_SYNC_FAILED    | 'some lock details'                            || MODULE_SYNC_FAILED
140     }
141
142     def 'Reset failed CM Handles #scenario.'() {
143         given: 'cm handles in an locked state'
144             def lockedState = new CompositeStateBuilder().withCmHandleState(CmHandleState.LOCKED)
145                     .withLockReason(LockReasonCategory.MODULE_SYNC_FAILED, '').withLastUpdatedTimeNow().build()
146             def yangModelCmHandle1 = new YangModelCmHandle(id: 'cm-handle-1', compositeState: lockedState)
147             def yangModelCmHandle2 = new YangModelCmHandle(id: 'cm-handle-2', compositeState: lockedState)
148             def expectedCmHandleStatePerCmHandle = [(yangModelCmHandle1): CmHandleState.ADVISED]
149         and: 'clear in progress map'
150             resetModuleSyncStartedOnCmHandles(moduleSyncStartedOnCmHandles)
151         and: 'add cm handle entry into progress map'
152             moduleSyncStartedOnCmHandles.put('cm-handle-1', 'started')
153             moduleSyncStartedOnCmHandles.put('cm-handle-2', 'started')
154         and: 'sync utils retry locked cm handle returns #isReadyForRetry'
155             mockSyncUtils.needsModuleSyncRetryOrUpgrade(lockedState) >>> isReadyForRetry
156         when: 'resetting failed cm handles'
157             objectUnderTest.resetFailedCmHandles([yangModelCmHandle1, yangModelCmHandle2])
158         then: 'updated to state "ADVISED" from "READY" is called as often as there are cm handles ready for retry'
159             expectedNumberOfInvocationsToUpdateCmHandleState * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(expectedCmHandleStatePerCmHandle)
160         and: 'after reset performed size of in progress map'
161             assert moduleSyncStartedOnCmHandles.size() == inProgressMapSize
162         where:
163             scenario                        | isReadyForRetry | inProgressMapSize || expectedNumberOfInvocationsToUpdateCmHandleState
164             'retry locked cm handle'        | [true, false]   | 1                 || 1
165             'do not retry locked cm handle' | [false, false]  | 2                 || 0
166     }
167
168     def 'Module Sync ADVISED cm handle without entry in progress map.'() {
169         given: 'cm handles in an ADVISED state'
170             def cmHandle1 = cmHandleAsDataNodeByIdAndState('cm-handle-1', CmHandleState.ADVISED)
171         and: 'the inventory persistence cm handle returns a ADVISED state for the any handle'
172             mockInventoryPersistence.getCmHandleState(_) >> new CompositeState(cmHandleState: CmHandleState.ADVISED)
173         and: 'entry in progress map for other cm handle'
174             moduleSyncStartedOnCmHandles.put('other-cm-handle', 'started')
175         when: 'module sync poll is executed'
176             objectUnderTest.performModuleSync([cmHandle1], batchCount)
177         then: 'module sync service is invoked for cm handle'
178             1 * mockModuleSyncService.syncAndCreateOrUpgradeSchemaSetAndAnchor(_) >> { args -> assertYamgModelCmHandleArgument(args, 'cm-handle-1') }
179         and: 'the entry for other cm handle is still in the progress map'
180             assert moduleSyncStartedOnCmHandles.get('other-cm-handle') != null
181     }
182
183     def 'Remove already processed cm handle id from hazelcast map'() {
184         given: 'hazelcast map contains cm handle id'
185             moduleSyncStartedOnCmHandles.put('ch-1', 'started')
186         when: 'remove cm handle entry'
187             objectUnderTest.removeResetCmHandleFromModuleSyncMap('ch-1')
188         then: 'an event is logged with level INFO'
189             def loggingEvent = getLoggingEvent()
190             assert loggingEvent.level == Level.INFO
191         and: 'the log indicates the cm handle entry is removed successfully'
192             assert loggingEvent.formattedMessage == 'ch-1 removed from in progress map'
193     }
194
195     def 'Remove non-existing cm handle id from hazelcast map'() {
196         given: 'hazelcast map does not contains cm handle id'
197             def result = moduleSyncStartedOnCmHandles.get('non-existing-cm-handle')
198             assert result == null
199         when: 'remove cm handle entry from  hazelcast map'
200             objectUnderTest.removeResetCmHandleFromModuleSyncMap('non-existing-cm-handle')
201         then: 'no event is logged'
202             def loggingEvent = getLoggingEvent()
203             assert loggingEvent == null
204     }
205
206     def cmHandleAsDataNodeByIdAndState(cmHandleId, cmHandleState) {
207         return new DataNode(anchorName: cmHandleId, leaves: ['id': cmHandleId, 'cm-handle-state': cmHandleState])
208     }
209
210     def assertYamgModelCmHandleArgument(args, expectedCmHandleId) {
211         {
212             def yangModelCmHandle = args[0]
213             assert yangModelCmHandle.id == expectedCmHandleId
214         }
215         return true
216     }
217
218     def assertBatch(args, expectedCmHandleStatePerCmHandleIds, expectedCmHandleState) {
219         {
220             Map<YangModelCmHandle, CmHandleState> actualCmHandleStatePerCmHandle = args[0]
221             assert actualCmHandleStatePerCmHandle.size() == expectedCmHandleStatePerCmHandleIds.size()
222             actualCmHandleStatePerCmHandle.each {
223                 assert expectedCmHandleStatePerCmHandleIds.contains(it.key.id)
224                 assert it.value == expectedCmHandleState
225             }
226         }
227         return true
228     }
229
230     def resetModuleSyncStartedOnCmHandles(moduleSyncStartedOnCmHandles) {
231         moduleSyncStartedOnCmHandles.clear();
232     }
233
234     def getLoggingEvent() {
235         return logger.list[0]
236     }
237 }