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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 * SPDX-License-Identifier: Apache-2.0
18 * ============LICENSE_END=========================================================
21 package org.onap.cps.ncmp.impl.inventory.sync.lcm
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
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
43 class LcmEventsCmHandleStateHandlerImplSpec extends Specification {
45 def logger = Spy(ListAppender<ILoggingEvent>)
48 ((Logger) LoggerFactory.getLogger(LcmEventsCmHandleStateHandlerImpl.class)).addAppender(logger)
53 ((Logger) LoggerFactory.getLogger(LcmEventsCmHandleStateHandlerImpl.class)).detachAndStopAllAppenders()
56 def mockInventoryPersistence = Mock(InventoryPersistence)
57 def mockLcmEventsCreator = Mock(LcmEventsCreator)
58 def mockLcmEventsService = Mock(LcmEventsService)
60 def lcmEventsCmHandleStateHandlerAsyncHelper = new LcmEventsCmHandleStateHandlerAsyncHelper(mockLcmEventsCreator, mockLcmEventsService)
61 def objectUnderTest = new LcmEventsCmHandleStateHandlerImpl(mockInventoryPersistence, lcmEventsCmHandleStateHandlerAsyncHelper)
63 def cmHandleId = 'cmhandle-id-1'
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(_) >> {
76 def cmHandleStatePerCmHandleId = args[0] as Map<String, CompositeState>
77 assert cmHandleStatePerCmHandleId.get(cmHandleId).cmHandleState == toCmHandleState
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
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"
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(_) >> {
117 def cmHandleStatePerCmHandleId = args[0] as Map<String, CompositeState>
118 assert cmHandleStatePerCmHandleId.get(cmHandleId).lockReason.details == 'some lock details'
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"
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(_) >> {
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
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"
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, _, _)
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, _, _)
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
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(_) >> {
197 assert (args[0] as Collection<YangModelCmHandle>).id.containsAll('cmhandle1', 'cmhandle2')
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'
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(_) >> {
217 assert (args[0] as Map<String, CompositeState>).keySet().containsAll(['cmhandle1', 'cmhandle2'])
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'
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'
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
260 def setupBatch(type) {
262 def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1', dmiProperties: [], publicProperties: [])
263 def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2', dmiProperties: [], publicProperties: [])
267 return [yangModelCmHandle1, yangModelCmHandle2]
270 yangModelCmHandle1.compositeState = new CompositeState(cmHandleState: READY)
271 yangModelCmHandle2.compositeState = new CompositeState(cmHandleState: READY)
272 return [(yangModelCmHandle1): DELETED, (yangModelCmHandle2): DELETED]
275 yangModelCmHandle1.compositeState = new CompositeState(cmHandleState: ADVISED)
276 yangModelCmHandle2.compositeState = new CompositeState(cmHandleState: READY)
277 return [(yangModelCmHandle1): READY, (yangModelCmHandle2): DELETING]
280 yangModelCmHandle1.compositeState = new CompositeState(cmHandleState: ADVISED)
281 yangModelCmHandle2.compositeState = new CompositeState(cmHandleState: READY)
282 return [(yangModelCmHandle1): ADVISED, (yangModelCmHandle2): READY]
285 throw new IllegalArgumentException("batch type '${type}' not recognized")
289 def getLogMessage(index) {
290 return logger.list[index].formattedMessage