827af6110baa90408fccdb2a6d1cfdd3db0ff82b
[cps.git] /
1 /*
2  * ============LICENSE_START=======================================================
3  * Copyright (C) 2022-2025 OpenInfra Foundation Europe. All rights reserved.
4  * ================================================================================
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *       http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  *
17  * SPDX-License-Identifier: Apache-2.0
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.onap.cps.ncmp.impl.inventory.sync.lcm
22
23 import ch.qos.logback.classic.Level
24 import ch.qos.logback.classic.Logger
25 import ch.qos.logback.classic.spi.ILoggingEvent
26 import ch.qos.logback.core.read.ListAppender
27 import org.onap.cps.ncmp.api.inventory.DataStoreSyncState
28 import org.onap.cps.ncmp.api.inventory.models.CompositeState
29 import org.onap.cps.ncmp.impl.inventory.InventoryPersistence
30 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
31 import org.slf4j.LoggerFactory
32 import spock.lang.Specification
33
34 import static java.util.Collections.EMPTY_LIST
35 import static java.util.Collections.EMPTY_MAP
36 import static org.onap.cps.ncmp.api.inventory.models.CmHandleState.ADVISED
37 import static org.onap.cps.ncmp.api.inventory.models.CmHandleState.DELETED
38 import static org.onap.cps.ncmp.api.inventory.models.CmHandleState.DELETING
39 import static org.onap.cps.ncmp.api.inventory.models.CmHandleState.LOCKED
40 import static org.onap.cps.ncmp.api.inventory.models.CmHandleState.READY
41 import static org.onap.cps.ncmp.api.inventory.models.LockReasonCategory.MODULE_SYNC_FAILED
42
43 class LcmEventsCmHandleStateHandlerImplSpec extends Specification {
44
45     def logAppender = Spy(ListAppender<ILoggingEvent>)
46
47     void setup() {
48         def logger = LoggerFactory.getLogger(LcmEventsCmHandleStateHandlerImpl)
49         logger.setLevel(Level.DEBUG)
50         logger.addAppender(logAppender)
51         logAppender.start()
52     }
53
54     void cleanup() {
55         ((Logger) LoggerFactory.getLogger(LcmEventsCmHandleStateHandlerImpl.class)).detachAndStopAllAppenders()
56     }
57
58     def mockInventoryPersistence = Mock(InventoryPersistence)
59     def mockLcmEventsCreator = Mock(LcmEventsProducerHelper)
60     def mockLcmEventsProducer = Mock(LcmEventsProducer)
61     def mockCmHandleStateMonitor = Mock(CmHandleStateMonitor)
62
63     def lcmEventsHelper = new LcmEventsHelper(mockLcmEventsCreator, mockLcmEventsProducer)
64     def objectUnderTest = new LcmEventsCmHandleStateHandlerImpl(mockInventoryPersistence, lcmEventsHelper, mockCmHandleStateMonitor)
65
66     def cmHandleId = 'cmhandle-id-1'
67     def currentCompositeState
68     def yangModelCmHandle
69
70     def 'Update and Send Events on State Change #stateChange'() {
71         given: 'Cm Handle represented as YangModelCmHandle'
72             currentCompositeState = new CompositeState(cmHandleState: fromCmHandleState)
73             yangModelCmHandle = new YangModelCmHandle(id: cmHandleId, additionalProperties: [], publicProperties: [], compositeState: currentCompositeState)
74         when: 'update state is invoked'
75             objectUnderTest.updateCmHandleStateBatch([(yangModelCmHandle): toCmHandleState])
76         then: 'state is saved using inventory persistence'
77             1 * mockInventoryPersistence.saveCmHandleStateBatch(cmHandleStatePerCmHandleId -> {
78                     assert cmHandleStatePerCmHandleId.get(cmHandleId).cmHandleState == toCmHandleState
79                 })
80         and: 'log message shows state change at DEBUG level'
81             def loggingEvent = logAppender.list[0]
82             assert loggingEvent.level == Level.DEBUG
83             assert loggingEvent.formattedMessage == "${cmHandleId} is now in ${toCmHandleState} state"
84         and: 'event service is called to send event'
85             1 * mockLcmEventsProducer.sendLcmEvent(cmHandleId, _, _)
86         where: 'state change parameters are provided'
87             stateChange           | fromCmHandleState | toCmHandleState
88             'ADVISED to READY'    | ADVISED           | READY
89             'READY to LOCKED'     | READY             | LOCKED
90             'ADVISED to LOCKED'   | ADVISED           | LOCKED
91             'ADVISED to DELETING' | ADVISED           | DELETING
92     }
93
94     def 'Update and Send Events on State Change from non-existing to ADVISED'() {
95         given: 'Cm Handle represented as YangModelCmHandle'
96             yangModelCmHandle = new YangModelCmHandle(id: cmHandleId, additionalProperties: [], publicProperties: [])
97         when: 'update state is invoked'
98             objectUnderTest.updateCmHandleStateBatch(Map.of(yangModelCmHandle, ADVISED))
99         then: 'CM-handle is saved using inventory persistence'
100             1 * mockInventoryPersistence.saveCmHandleBatch(List.of(yangModelCmHandle))
101         and: 'event service is called to send event'
102             1 * mockLcmEventsProducer.sendLcmEvent(cmHandleId, _, _)
103         and: 'a log entry is written'
104             assert getLogMessage(0) == "${cmHandleId} is now in ADVISED state"
105     }
106
107     def 'Update and Send Events on State Change from LOCKED to ADVISED'() {
108         given: 'Cm Handle represented as YangModelCmHandle in LOCKED state'
109             currentCompositeState = new CompositeState(cmHandleState: LOCKED, lockReason: CompositeState.LockReason.builder().lockReasonCategory(MODULE_SYNC_FAILED).details('some lock details').build())
110             yangModelCmHandle = new YangModelCmHandle(id: cmHandleId, additionalProperties: [], publicProperties: [], compositeState: currentCompositeState)
111         when: 'update state is invoked'
112             objectUnderTest.updateCmHandleStateBatch([(yangModelCmHandle): ADVISED])
113         then: 'state is saved using inventory persistence and old lock reason details are retained'
114             1 * mockInventoryPersistence.saveCmHandleStateBatch(cmHandleStatePerCmHandleId -> {
115                     assert cmHandleStatePerCmHandleId.get(cmHandleId).lockReason.details == 'some lock details'
116                 })
117         and: 'event service is called to send event'
118             1 * mockLcmEventsProducer.sendLcmEvent(cmHandleId, _, _)
119         and: 'a log entry is written'
120             assert getLogMessage(0) == "${cmHandleId} is now in ADVISED state"
121     }
122
123     def 'Update and Send Events on State Change to from ADVISED to READY'() {
124         given: 'Cm Handle represented as YangModelCmHandle'
125             currentCompositeState = new CompositeState(cmHandleState: ADVISED)
126             yangModelCmHandle = new YangModelCmHandle(id: cmHandleId, additionalProperties: [], publicProperties: [], compositeState: currentCompositeState)
127         and: 'global sync flag is set'
128             currentCompositeState.setDataSyncEnabled(false)
129         when: 'update cmhandle state is invoked'
130             objectUnderTest.updateCmHandleStateBatch(Map.of(yangModelCmHandle, READY))
131         then: 'state is saved using inventory persistence with expected dataSyncState'
132             1 * mockInventoryPersistence.saveCmHandleStateBatch(cmHandleStatePerCmHandleId -> {
133                     assert cmHandleStatePerCmHandleId.get(cmHandleId).dataSyncEnabled == false
134                     assert cmHandleStatePerCmHandleId.get(cmHandleId).dataStores.operationalDataStore.dataStoreSyncState == DataStoreSyncState.NONE_REQUESTED
135                 })
136         and: 'event service is called to send event'
137             1 * mockLcmEventsProducer.sendLcmEvent(cmHandleId, _, _)
138         and: 'a log entry is written'
139             assert getLogMessage(0) == "${cmHandleId} is now in READY state"
140     }
141
142     def 'Update cmHandle state from READY to DELETING' (){
143         given: 'cm Handle as Yang model'
144             currentCompositeState = new CompositeState(cmHandleState: READY)
145             yangModelCmHandle = new YangModelCmHandle(id: cmHandleId, additionalProperties: [], publicProperties: [], compositeState: currentCompositeState)
146         when: 'updating cm handle state to "DELETING"'
147             objectUnderTest.updateCmHandleStateBatch([(yangModelCmHandle): DELETING])
148         then: 'the cm handle state is as expected'
149             yangModelCmHandle.getCompositeState().getCmHandleState() == DELETING
150         and: 'method to persist cm handle state is called once'
151             1 * mockInventoryPersistence.saveCmHandleStateBatch([(cmHandleId): yangModelCmHandle.compositeState])
152         and: 'the method to send Lcm event is called once'
153             1 * mockLcmEventsProducer.sendLcmEvent(cmHandleId, _, _)
154     }
155
156     def 'Update cmHandle state to DELETING to DELETED' (){
157         given: 'cm Handle with state "DELETING" as Yang model '
158             currentCompositeState = new CompositeState(cmHandleState: DELETING)
159             yangModelCmHandle = new YangModelCmHandle(id: cmHandleId, additionalProperties: [], publicProperties: [], compositeState: currentCompositeState)
160         when: 'updating cm handle state to "DELETED"'
161             objectUnderTest.updateCmHandleStateBatch([(yangModelCmHandle): DELETED])
162         then: 'the cm handle state is as expected'
163             yangModelCmHandle.getCompositeState().getCmHandleState() == DELETED
164         and: 'the method to send Lcm event is called once'
165             1 * mockLcmEventsProducer.sendLcmEvent(cmHandleId, _, _)
166     }
167
168     def 'No state change and no event to be sent'() {
169         given: 'Cm Handle batch with same state transition as before'
170             def cmHandleStateMap = setupBatch('NO_CHANGE')
171         when: 'updating a batch of changes'
172             objectUnderTest.updateCmHandleStateBatch(cmHandleStateMap)
173         then: 'no changes are persisted'
174             1 * mockInventoryPersistence.saveCmHandleBatch(EMPTY_LIST)
175             1 * mockInventoryPersistence.saveCmHandleStateBatch(EMPTY_MAP)
176         and: 'no event will be sent'
177             0 * mockLcmEventsProducer.sendLcmEvent(*_)
178     }
179
180     def 'Batch of new cm handles provided'() {
181         given: 'A batch of new cm handles'
182             def yangModelCmHandlesToBeCreated = setupBatch('NEW')
183         when: 'instantiating a batch of new cm handles'
184             objectUnderTest.initiateStateAdvised(yangModelCmHandlesToBeCreated)
185         then: 'new cm handles are saved using inventory persistence'
186             1 * mockInventoryPersistence.saveCmHandleBatch(yangModelCmHandles -> {
187                     assert yangModelCmHandles.id.containsAll('cmhandle1', 'cmhandle2')
188                 })
189         and: 'no state updates are persisted'
190             1 * mockInventoryPersistence.saveCmHandleStateBatch(EMPTY_MAP)
191         and: 'event service is called to send events'
192             2 * mockLcmEventsProducer.sendLcmEvent(_, _, _)
193         and: 'two log entries are written'
194             assert getLogMessage(0) == 'cmhandle1 is now in ADVISED state'
195             assert getLogMessage(1) == 'cmhandle2 is now in ADVISED state'
196     }
197
198     def 'Batch of existing cm handles is updated'() {
199         given: 'A batch of updated cm handles'
200             def cmHandleStateMap = setupBatch('UPDATE')
201         when: 'updating a batch of changes'
202             objectUnderTest.updateCmHandleStateBatch(cmHandleStateMap)
203         then: 'existing cm handles composite states are persisted'
204             1 * mockInventoryPersistence.saveCmHandleStateBatch(cmHandleStatePerCmHandleId -> {
205                     assert cmHandleStatePerCmHandleId.keySet().containsAll(['cmhandle1', 'cmhandle2'])
206                 })
207         and: 'no new handles are persisted'
208             1 * mockInventoryPersistence.saveCmHandleBatch(EMPTY_LIST)
209         and: 'event service is called to send events'
210             2 * mockLcmEventsProducer.sendLcmEvent(_, _, _)
211         and: 'two log entries are written'
212             assert getLogMessage(0) == 'cmhandle1 is now in READY state'
213             assert getLogMessage(1) == 'cmhandle2 is now in DELETING state'
214     }
215
216     def 'Batch of existing cm handles is deleted'() {
217         given: 'A batch of deleted cm handles'
218             def cmHandleStateMap = setupBatch('DELETED')
219         when: 'updating a batch of changes'
220             objectUnderTest.updateCmHandleStateBatch(cmHandleStateMap)
221         then: 'state of deleted handles is not persisted'
222             1 * mockInventoryPersistence.saveCmHandleStateBatch(EMPTY_MAP)
223         and: 'no new handles are persisted'
224             1 * mockInventoryPersistence.saveCmHandleBatch(EMPTY_LIST)
225         and: 'event service is called to send events'
226             2 * mockLcmEventsProducer.sendLcmEvent(_, _, _)
227         and: 'two log entries are written'
228             assert getLogMessage(0) == 'cmhandle1 is now in DELETED state'
229             assert getLogMessage(1) == 'cmhandle2 is now in DELETED state'
230     }
231
232     def 'Log entries and events are not sent when an error occurs during persistence'() {
233         given: 'A batch of updated cm handles'
234             def cmHandleStateMap = setupBatch('UPDATE')
235         and: 'an error will be thrown when trying to persist'
236             mockInventoryPersistence.saveCmHandleStateBatch(_) >> { throw new RuntimeException() }
237         when: 'updating a batch of changes'
238             objectUnderTest.updateCmHandleStateBatch(cmHandleStateMap)
239         then: 'the exception is not handled'
240             thrown(RuntimeException)
241         and: 'no events are sent'
242             0 * mockLcmEventsProducer.sendLcmEvent(_, _, _)
243         and: 'no log entries are written'
244             assert logAppender.list.empty
245     }
246
247     def setupBatch(type) {
248
249         def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1', additionalProperties: [], publicProperties: [])
250         def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2', additionalProperties: [], publicProperties: [])
251
252         switch (type) {
253             case 'NEW':
254                 return [yangModelCmHandle1, yangModelCmHandle2]
255
256             case 'DELETED':
257                 yangModelCmHandle1.compositeState = new CompositeState(cmHandleState: READY)
258                 yangModelCmHandle2.compositeState = new CompositeState(cmHandleState: READY)
259                 return [(yangModelCmHandle1): DELETED, (yangModelCmHandle2): DELETED]
260
261             case 'UPDATE':
262                 yangModelCmHandle1.compositeState = new CompositeState(cmHandleState: ADVISED)
263                 yangModelCmHandle2.compositeState = new CompositeState(cmHandleState: READY)
264                 return [(yangModelCmHandle1): READY, (yangModelCmHandle2): DELETING]
265
266             case 'NO_CHANGE':
267                 yangModelCmHandle1.compositeState = new CompositeState(cmHandleState: ADVISED)
268                 yangModelCmHandle2.compositeState = new CompositeState(cmHandleState: READY)
269                 return [(yangModelCmHandle1): ADVISED, (yangModelCmHandle2): READY]
270
271             default:
272                 throw new IllegalArgumentException("batch type '${type}' not recognized")
273         }
274     }
275
276     def getLogMessage(index) {
277         return logAppender.list[index].formattedMessage
278     }
279 }