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
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.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
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
43 class LcmEventsCmHandleStateHandlerImplSpec extends Specification {
45 def logAppender = Spy(ListAppender<ILoggingEvent>)
48 def logger = LoggerFactory.getLogger(LcmEventsCmHandleStateHandlerImpl)
49 logger.setLevel(Level.DEBUG)
50 logger.addAppender(logAppender)
55 ((Logger) LoggerFactory.getLogger(LcmEventsCmHandleStateHandlerImpl.class)).detachAndStopAllAppenders()
58 def mockInventoryPersistence = Mock(InventoryPersistence)
59 def mockLcmEventsCreator = Mock(LcmEventsProducerHelper)
60 def mockLcmEventsProducer = Mock(LcmEventsProducer)
61 def mockCmHandleStateMonitor = Mock(CmHandleStateMonitor)
63 def lcmEventsHelper = new LcmEventsHelper(mockLcmEventsCreator, mockLcmEventsProducer)
64 def objectUnderTest = new LcmEventsCmHandleStateHandlerImpl(mockInventoryPersistence, lcmEventsHelper, mockCmHandleStateMonitor)
66 def cmHandleId = 'cmhandle-id-1'
67 def currentCompositeState
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
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
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"
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'
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"
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
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"
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, _, _)
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, _, _)
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(*_)
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')
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'
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'])
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'
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'
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
247 def setupBatch(type) {
249 def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1', additionalProperties: [], publicProperties: [])
250 def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2', additionalProperties: [], publicProperties: [])
254 return [yangModelCmHandle1, yangModelCmHandle2]
257 yangModelCmHandle1.compositeState = new CompositeState(cmHandleState: READY)
258 yangModelCmHandle2.compositeState = new CompositeState(cmHandleState: READY)
259 return [(yangModelCmHandle1): DELETED, (yangModelCmHandle2): DELETED]
262 yangModelCmHandle1.compositeState = new CompositeState(cmHandleState: ADVISED)
263 yangModelCmHandle2.compositeState = new CompositeState(cmHandleState: READY)
264 return [(yangModelCmHandle1): READY, (yangModelCmHandle2): DELETING]
267 yangModelCmHandle1.compositeState = new CompositeState(cmHandleState: ADVISED)
268 yangModelCmHandle2.compositeState = new CompositeState(cmHandleState: READY)
269 return [(yangModelCmHandle1): ADVISED, (yangModelCmHandle2): READY]
272 throw new IllegalArgumentException("batch type '${type}' not recognized")
276 def getLogMessage(index) {
277 return logAppender.list[index].formattedMessage