ebf2eee1201c619dfc46e6384ab1db6c1a002e53
[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.models.CompositeState
28 import org.onap.cps.ncmp.api.inventory.DataStoreSyncState
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 logger = Spy(ListAppender<ILoggingEvent>)
46
47     void setup() {
48         ((Logger) LoggerFactory.getLogger(LcmEventsCmHandleStateHandlerImpl.class)).addAppender(logger)
49         logger.start()
50     }
51
52     void cleanup() {
53         ((Logger) LoggerFactory.getLogger(LcmEventsCmHandleStateHandlerImpl.class)).detachAndStopAllAppenders()
54     }
55
56     def mockInventoryPersistence = Mock(InventoryPersistence)
57     def mockLcmEventsCreator = Mock(LcmEventsProducerHelper)
58     def mockLcmEventsProducer = Mock(LcmEventsProducer)
59     def mockCmHandleStateMonitor = Mock(CmHandleStateMonitor)
60
61     def lcmEventsCmHandleStateHandlerAsyncHelper = new LcmEventsCmHandleStateHandlerAsyncHelper(mockLcmEventsCreator, mockLcmEventsProducer)
62     def objectUnderTest = new LcmEventsCmHandleStateHandlerImpl(mockInventoryPersistence, lcmEventsCmHandleStateHandlerAsyncHelper, mockCmHandleStateMonitor)
63
64     def cmHandleId = 'cmhandle-id-1'
65     def compositeState
66     def yangModelCmHandle
67
68     def 'Update and Send Events on State Change #stateChange'() {
69         given: 'Cm Handle represented as YangModelCmHandle'
70             compositeState = new CompositeState(cmHandleState: fromCmHandleState)
71             yangModelCmHandle = new YangModelCmHandle(id: cmHandleId, dmiProperties: [], publicProperties: [], compositeState: compositeState)
72         when: 'update state is invoked'
73             objectUnderTest.updateCmHandleStateBatch(Map.of(yangModelCmHandle, toCmHandleState))
74         then: 'state is saved using inventory persistence'
75             1 * mockInventoryPersistence.saveCmHandleStateBatch(_) >> {
76                 args -> {
77                     def cmHandleStatePerCmHandleId = args[0] as Map<String, CompositeState>
78                     assert cmHandleStatePerCmHandleId.get(cmHandleId).cmHandleState == toCmHandleState
79                 }
80             }
81         and: 'log message shows state change at INFO level'
82             def loggingEvent = (ILoggingEvent) logger.list[0]
83             assert loggingEvent.level == Level.INFO
84             assert loggingEvent.formattedMessage == "${cmHandleId} is now in ${toCmHandleState} state"
85         and: 'event service is called to send event'
86             1 * mockLcmEventsProducer.sendLcmEvent(cmHandleId, _, _)
87         where: 'state change parameters are provided'
88             stateChange           | fromCmHandleState | toCmHandleState
89             'ADVISED to READY'    | ADVISED           | READY
90             'READY to LOCKED'     | READY             | LOCKED
91             'ADVISED to LOCKED'   | ADVISED           | LOCKED
92             'ADVISED to DELETING' | ADVISED           | DELETING
93     }
94
95     def 'Update and Send Events on State Change from non-existing to ADVISED'() {
96         given: 'Cm Handle represented as YangModelCmHandle'
97             yangModelCmHandle = new YangModelCmHandle(id: cmHandleId, dmiProperties: [], publicProperties: [])
98         when: 'update state is invoked'
99             objectUnderTest.updateCmHandleStateBatch(Map.of(yangModelCmHandle, ADVISED))
100         then: 'CM-handle is saved using inventory persistence'
101             1 * mockInventoryPersistence.saveCmHandleBatch(List.of(yangModelCmHandle))
102         and: 'event service is called to send event'
103             1 * mockLcmEventsProducer.sendLcmEvent(cmHandleId, _, _)
104         and: 'a log entry is written'
105             assert getLogMessage(0) == "${cmHandleId} is now in ADVISED state"
106     }
107
108     def 'Update and Send Events on State Change from LOCKED to ADVISED'() {
109         given: 'Cm Handle represented as YangModelCmHandle in LOCKED state'
110             compositeState = new CompositeState(cmHandleState: LOCKED,
111                 lockReason: CompositeState.LockReason.builder().lockReasonCategory(MODULE_SYNC_FAILED).details('some lock details').build())
112             yangModelCmHandle = new YangModelCmHandle(id: cmHandleId, dmiProperties: [], publicProperties: [], compositeState: compositeState)
113         when: 'update state is invoked'
114             objectUnderTest.updateCmHandleStateBatch(Map.of(yangModelCmHandle, ADVISED))
115         then: 'state is saved using inventory persistence and old lock reason details are retained'
116             1 * mockInventoryPersistence.saveCmHandleStateBatch(_) >> {
117                 args -> {
118                     def cmHandleStatePerCmHandleId = args[0] as Map<String, CompositeState>
119                     assert cmHandleStatePerCmHandleId.get(cmHandleId).lockReason.details == 'some lock details'
120                 }
121             }
122         and: 'event service is called to send event'
123             1 * mockLcmEventsProducer.sendLcmEvent(cmHandleId, _, _)
124         and: 'a log entry is written'
125             assert getLogMessage(0) == "${cmHandleId} is now in ADVISED state"
126     }
127
128     def 'Update and Send Events on State Change to from ADVISED to READY'() {
129         given: 'Cm Handle represented as YangModelCmHandle'
130             compositeState = new CompositeState(cmHandleState: ADVISED)
131             yangModelCmHandle = new YangModelCmHandle(id: cmHandleId, dmiProperties: [], publicProperties: [], compositeState: compositeState)
132         and: 'global sync flag is set'
133             compositeState.setDataSyncEnabled(false)
134         when: 'update cmhandle state is invoked'
135             objectUnderTest.updateCmHandleStateBatch(Map.of(yangModelCmHandle, READY))
136         then: 'state is saved using inventory persistence with expected dataSyncState'
137             1 * mockInventoryPersistence.saveCmHandleStateBatch(_) >> {
138                 args-> {
139                     def cmHandleStatePerCmHandleId = args[0] as Map<String, CompositeState>
140                     assert cmHandleStatePerCmHandleId.get(cmHandleId).dataSyncEnabled == false
141                     assert cmHandleStatePerCmHandleId.get(cmHandleId).dataStores.operationalDataStore.dataStoreSyncState == DataStoreSyncState.NONE_REQUESTED
142                 }
143             }
144         and: 'event service is called to send event'
145             1 * mockLcmEventsProducer.sendLcmEvent(cmHandleId, _, _)
146         and: 'a log entry is written'
147             assert getLogMessage(0) == "${cmHandleId} is now in READY state"
148     }
149
150     def 'Update cmHandle state from READY to DELETING' (){
151         given: 'cm Handle as Yang model'
152             compositeState = new CompositeState(cmHandleState: READY)
153             yangModelCmHandle = new YangModelCmHandle(id: cmHandleId, dmiProperties: [], publicProperties: [], compositeState: compositeState)
154         when: 'updating cm handle state to "DELETING"'
155             objectUnderTest.updateCmHandleStateBatch(Map.of(yangModelCmHandle, DELETING))
156         then: 'the cm handle state is as expected'
157             yangModelCmHandle.getCompositeState().getCmHandleState() == DELETING
158         and: 'method to persist cm handle state is called once'
159             1 * mockInventoryPersistence.saveCmHandleStateBatch(Map.of(yangModelCmHandle.getId(), yangModelCmHandle.getCompositeState()))
160         and: 'the method to send Lcm event is called once'
161             1 * mockLcmEventsProducer.sendLcmEvent(cmHandleId, _, _)
162     }
163
164     def 'Update cmHandle state to DELETING to DELETED' (){
165         given: 'cm Handle with state "DELETING" as Yang model '
166             compositeState = new CompositeState(cmHandleState: DELETING)
167             yangModelCmHandle = new YangModelCmHandle(id: cmHandleId, dmiProperties: [], publicProperties: [], compositeState: compositeState)
168         when: 'updating cm handle state to "DELETED"'
169             objectUnderTest.updateCmHandleStateBatch(Map.of(yangModelCmHandle, DELETED))
170         then: 'the cm handle state is as expected'
171             yangModelCmHandle.getCompositeState().getCmHandleState() == DELETED
172         and: 'the method to send Lcm event is called once'
173             1 * mockLcmEventsProducer.sendLcmEvent(cmHandleId, _, _)
174     }
175
176     def 'No state change and no event to be sent'() {
177         given: 'Cm Handle batch with same state transition as before'
178             def cmHandleStateMap = setupBatch('NO_CHANGE')
179         when: 'updating a batch of changes'
180             objectUnderTest.updateCmHandleStateBatch(cmHandleStateMap)
181         then: 'no changes are persisted'
182             1 * mockInventoryPersistence.saveCmHandleBatch(EMPTY_LIST)
183             1 * mockInventoryPersistence.saveCmHandleStateBatch(EMPTY_MAP)
184         and: 'no event will be sent'
185             0 * mockLcmEventsProducer.sendLcmEvent(*_)
186         and: 'no log entries are written'
187             assert logger.list.empty
188     }
189
190     def 'Batch of new cm handles provided'() {
191         given: 'A batch of new cm handles'
192             def yangModelCmHandlesToBeCreated = setupBatch('NEW')
193         when: 'instantiating a batch of new cm handles'
194             objectUnderTest.initiateStateAdvised(yangModelCmHandlesToBeCreated)
195         then: 'new cm handles are saved using inventory persistence'
196             1 * mockInventoryPersistence.saveCmHandleBatch(_) >> {
197                 args -> {
198                     assert (args[0] as Collection<YangModelCmHandle>).id.containsAll('cmhandle1', 'cmhandle2')
199                 }
200             }
201         and: 'no state updates are persisted'
202             1 * mockInventoryPersistence.saveCmHandleStateBatch(EMPTY_MAP)
203         and: 'event service is called to send events'
204             2 * mockLcmEventsProducer.sendLcmEvent(_, _, _)
205         and: 'two log entries are written'
206             assert getLogMessage(0) == 'cmhandle1 is now in ADVISED state'
207             assert getLogMessage(1) == 'cmhandle2 is now in ADVISED state'
208     }
209
210     def 'Batch of existing cm handles is updated'() {
211         given: 'A batch of updated cm handles'
212             def cmHandleStateMap = setupBatch('UPDATE')
213         when: 'updating a batch of changes'
214             objectUnderTest.updateCmHandleStateBatch(cmHandleStateMap)
215         then: 'existing cm handles composite states are persisted'
216             1 * mockInventoryPersistence.saveCmHandleStateBatch(_) >> {
217                 args -> {
218                     assert (args[0] as Map<String, CompositeState>).keySet().containsAll(['cmhandle1', 'cmhandle2'])
219                 }
220             }
221         and: 'no new handles are persisted'
222             1 * mockInventoryPersistence.saveCmHandleBatch(EMPTY_LIST)
223         and: 'event service is called to send events'
224             2 * mockLcmEventsProducer.sendLcmEvent(_, _, _)
225         and: 'two log entries are written'
226             assert getLogMessage(0) == 'cmhandle1 is now in READY state'
227             assert getLogMessage(1) == 'cmhandle2 is now in DELETING state'
228     }
229
230     def 'Batch of existing cm handles is deleted'() {
231         given: 'A batch of deleted cm handles'
232             def cmHandleStateMap = setupBatch('DELETED')
233         when: 'updating a batch of changes'
234             objectUnderTest.updateCmHandleStateBatch(cmHandleStateMap)
235         then: 'state of deleted handles is not persisted'
236             1 * mockInventoryPersistence.saveCmHandleStateBatch(EMPTY_MAP)
237         and: 'no new handles are persisted'
238             1 * mockInventoryPersistence.saveCmHandleBatch(EMPTY_LIST)
239         and: 'event service is called to send events'
240             2 * mockLcmEventsProducer.sendLcmEvent(_, _, _)
241         and: 'two log entries are written'
242             assert getLogMessage(0) == 'cmhandle1 is now in DELETED state'
243             assert getLogMessage(1) == 'cmhandle2 is now in DELETED state'
244     }
245
246     def 'Log entries and events are not sent when an error occurs during persistence'() {
247         given: 'A batch of updated cm handles'
248             def cmHandleStateMap = setupBatch('UPDATE')
249         and: 'an error will be thrown when trying to persist'
250             mockInventoryPersistence.saveCmHandleStateBatch(_) >> { throw new RuntimeException() }
251         when: 'updating a batch of changes'
252             objectUnderTest.updateCmHandleStateBatch(cmHandleStateMap)
253         then: 'the exception is not handled'
254             thrown(RuntimeException)
255         and: 'no events are sent'
256             0 * mockLcmEventsProducer.sendLcmEvent(_, _, _)
257         and: 'no log entries are written'
258             assert logger.list.empty
259     }
260
261     def setupBatch(type) {
262
263         def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1', dmiProperties: [], publicProperties: [])
264         def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2', dmiProperties: [], publicProperties: [])
265
266         switch (type) {
267             case 'NEW':
268                 return [yangModelCmHandle1, yangModelCmHandle2]
269
270             case 'DELETED':
271                 yangModelCmHandle1.compositeState = new CompositeState(cmHandleState: READY)
272                 yangModelCmHandle2.compositeState = new CompositeState(cmHandleState: READY)
273                 return [(yangModelCmHandle1): DELETED, (yangModelCmHandle2): DELETED]
274
275             case 'UPDATE':
276                 yangModelCmHandle1.compositeState = new CompositeState(cmHandleState: ADVISED)
277                 yangModelCmHandle2.compositeState = new CompositeState(cmHandleState: READY)
278                 return [(yangModelCmHandle1): READY, (yangModelCmHandle2): DELETING]
279
280             case 'NO_CHANGE':
281                 yangModelCmHandle1.compositeState = new CompositeState(cmHandleState: ADVISED)
282                 yangModelCmHandle2.compositeState = new CompositeState(cmHandleState: READY)
283                 return [(yangModelCmHandle1): ADVISED, (yangModelCmHandle2): READY]
284
285             default:
286                 throw new IllegalArgumentException("batch type '${type}' not recognized")
287         }
288     }
289
290     def getLogMessage(index) {
291         return logger.list[index].formattedMessage
292     }
293 }