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