2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2021-2023 Nordix Foundation
4 * Modifications Copyright (C) 2021 Pantheon.tech
5 * Modifications Copyright (C) 2021-2022 Bell Canada
6 * Modifications Copyright (C) 2023 TechMahindra Ltd.
7 * ================================================================================
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
20 * SPDX-License-Identifier: Apache-2.0
21 * ============LICENSE_END=========================================================
24 package org.onap.cps.ncmp.api.impl
26 import com.hazelcast.map.IMap
27 import org.onap.cps.ncmp.api.NetworkCmProxyCmHandleQueryService
28 import org.onap.cps.ncmp.api.impl.events.lcm.LcmEventsCmHandleStateHandler
29 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
30 import org.onap.cps.ncmp.api.inventory.CmHandleQueries
31 import org.onap.cps.ncmp.api.inventory.CmHandleState
32 import org.onap.cps.ncmp.api.inventory.CompositeState
33 import org.onap.cps.ncmp.api.inventory.InventoryPersistence
34 import org.onap.cps.ncmp.api.inventory.LockReasonCategory
35 import org.onap.cps.ncmp.api.inventory.DataStoreSyncState
36 import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters
37 import org.onap.cps.ncmp.api.models.CmHandleQueryServiceParameters
38 import org.onap.cps.ncmp.api.models.ConditionApiProperties
39 import org.onap.cps.ncmp.api.models.DmiPluginRegistration
40 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
41 import org.onap.cps.spi.exceptions.CpsException
42 import org.onap.cps.spi.model.ConditionProperties
43 import spock.lang.Shared
44 import java.util.stream.Collectors
45 import org.onap.cps.utils.JsonObjectMapper
46 import com.fasterxml.jackson.databind.ObjectMapper
47 import org.onap.cps.api.CpsDataService
48 import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
49 import org.onap.cps.spi.FetchDescendantsOption
50 import org.onap.cps.spi.model.DataNode
51 import org.springframework.http.HttpStatus
52 import org.springframework.http.ResponseEntity
53 import spock.lang.Specification
55 import static org.onap.cps.ncmp.api.impl.operations.DataStoreEnum.PASSTHROUGH_OPERATIONAL
56 import static org.onap.cps.ncmp.api.impl.operations.DataStoreEnum.PASSTHROUGH_RUNNING
57 import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.CREATE
58 import static org.onap.cps.ncmp.api.impl.operations.OperationEnum.UPDATE
60 class NetworkCmProxyDataServiceImplSpec extends Specification {
62 def mockCpsDataService = Mock(CpsDataService)
63 def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
64 def mockDmiDataOperations = Mock(DmiDataOperations)
65 def nullNetworkCmProxyDataServicePropertyHandler = null
66 def mockInventoryPersistence = Mock(InventoryPersistence)
67 def mockCmHandleQueries = Mock(CmHandleQueries)
68 def mockDmiPluginRegistration = Mock(DmiPluginRegistration)
69 def mockCpsCmHandlerQueryService = Mock(NetworkCmProxyCmHandleQueryService)
70 def mockLcmEventsCmHandleStateHandler = Mock(LcmEventsCmHandleStateHandler)
71 def stubModuleSyncStartedOnCmHandles = Stub(IMap<String, Object>)
74 def NO_REQUEST_ID = null
76 def OPTIONS_PARAM = '(a=1,b=2)'
78 def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'test-cm-handle-id')
80 def objectUnderTest = new NetworkCmProxyDataServiceImpl(
81 spiedJsonObjectMapper,
82 mockDmiDataOperations,
83 nullNetworkCmProxyDataServicePropertyHandler,
84 mockInventoryPersistence,
86 mockCpsCmHandlerQueryService,
87 mockLcmEventsCmHandleStateHandler,
89 stubModuleSyncStartedOnCmHandles)
91 def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
93 def dataNode = [new DataNode(leaves: ['id': 'some-cm-handle', 'dmi-service-name': 'testDmiService'])]
95 def 'Write resource data for pass-through running from DMI using POST.'() {
96 given: 'cpsDataService returns valid datanode'
98 when: 'write resource data is called'
99 objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
100 'testResourceId', CREATE,
101 '{some-json}', 'application/json')
102 then: 'DMI called with correct data'
103 1 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi('testCmHandle', 'testResourceId',
104 CREATE, '{some-json}', 'application/json')
105 >> { new ResponseEntity<>(HttpStatus.CREATED) }
108 def 'Get resource data for pass-through operational from DMI.'() {
109 given: 'get data node is called'
111 and: 'get resource data from DMI is called'
112 mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_OPERATIONAL.value,'testCmHandle',
113 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID) >>
114 new ResponseEntity<>('dmi-response', HttpStatus.OK)
115 when: 'get resource data operational for cm-handle is called'
116 def response = objectUnderTest.getResourceDataForCmHandle(PASSTHROUGH_OPERATIONAL.value, 'testCmHandle',
117 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID)
118 then: 'DMI returns a json response'
119 response == 'dmi-response'
122 def 'Get resource data for pass-through running from DMI.'() {
123 given: 'cpsDataService returns valid data node'
125 and: 'DMI returns valid response and data'
126 mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_RUNNING.value, 'testCmHandle',
127 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID) >>
128 new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
129 when: 'get resource data is called'
130 def response = objectUnderTest.getResourceDataForCmHandle(PASSTHROUGH_RUNNING.value, 'testCmHandle',
131 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID)
132 then: 'get resource data returns expected response'
133 response == '{dmi-response}'
136 def 'Get bulk resource data for #datastoreName from DMI.'() {
137 given: 'cpsDataService returns valid data node'
139 and: 'DMI returns valid response and data'
140 mockDmiDataOperations.getResourceDataFromDmi(datastoreName, ['testCmHandle'],
141 'testResourceId', OPTIONS_PARAM,'some topic','requestId') >>
142 new ResponseEntity<>('{dmi-bulk-response}', HttpStatus.OK)
143 when: 'get batch resource data is called'
144 def response = objectUnderTest.getResourceDataForCmHandleBatch(datastoreName, ['testCmHandle'],
149 then: 'get bulk resource data returns expected response'
150 response == '{dmi-bulk-response}'
151 where: 'the following data stores are used'
152 datastoreName << [PASSTHROUGH_RUNNING.value, PASSTHROUGH_OPERATIONAL.value]
155 def 'Getting Yang Resources.'() {
156 when: 'yang resources is called'
157 objectUnderTest.getYangResourcesModuleReferences('some-cm-handle')
158 then: 'CPS module services is invoked for the correct dataspace and cm handle'
159 1 * mockInventoryPersistence.getYangResourcesModuleReferences('some-cm-handle')
162 def 'Get a cm handle.'() {
163 given: 'the system returns a yang modelled cm handle'
164 def dmiServiceName = 'some service name'
165 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED,
166 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.LOCKED_MODULE_SYNC_FAILED).details("lock details").build(),
167 lastUpdateTime: 'some-timestamp',
168 dataSyncEnabled: false,
169 dataStores: dataStores())
170 def dmiProperties = [new YangModelCmHandle.Property('Book', 'Romance Novel')]
171 def publicProperties = [new YangModelCmHandle.Property('Public Book', 'Public Romance Novel')]
172 def yangModelCmHandle = new YangModelCmHandle(id: 'some-cm-handle', dmiServiceName: dmiServiceName,
173 dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
174 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
175 when: 'getting cm handle details for a given cm handle id from ncmp service'
176 def result = objectUnderTest.getNcmpServiceCmHandle('some-cm-handle')
177 then: 'the result is a ncmpServiceCmHandle'
178 result.class == NcmpServiceCmHandle.class
179 and: 'the cm handle contains the cm handle id'
180 result.cmHandleId == 'some-cm-handle'
181 and: 'the cm handle contains the DMI Properties'
182 result.dmiProperties ==[ Book:'Romance Novel' ]
183 and: 'the cm handle contains the public Properties'
184 result.publicProperties == [ "Public Book":'Public Romance Novel' ]
185 and: 'the cm handle contains the cm handle composite state'
186 result.compositeState == compositeState
190 def 'Get cm handle public properties'() {
191 given: 'a yang modelled cm handle'
192 def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
193 def publicProperties = [new YangModelCmHandle.Property('public prop', 'some public prop')]
194 def yangModelCmHandle = new YangModelCmHandle(id:'some-cm-handle', dmiServiceName: 'some service name', dmiProperties: dmiProperties, publicProperties: publicProperties)
195 and: 'the system returns this yang modelled cm handle'
196 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
197 when: 'getting cm handle public properties for a given cm handle id from ncmp service'
198 def result = objectUnderTest.getCmHandlePublicProperties('some-cm-handle')
199 then: 'the result returns the correct data'
200 result == [ 'public prop' : 'some public prop' ]
203 def 'Execute cm handle id search for inventory'() {
204 given: 'a ConditionApiProperties object'
205 def conditionProperties = new ConditionProperties()
206 conditionProperties.conditionName = 'hasAllProperties'
207 conditionProperties.conditionParameters = [ [ 'some-key' : 'some-value' ] ]
208 def conditionServiceProps = new CmHandleQueryServiceParameters()
209 conditionServiceProps.cmHandleQueryParameters = [conditionProperties] as List<ConditionProperties>
210 and: 'the system returns an set of cmHandle ids'
211 mockCpsCmHandlerQueryService.queryCmHandleIdsForInventory(*_) >> [ 'cmHandle1', 'cmHandle2' ]
212 when: 'getting cm handle id set for a given dmi property'
213 def result = objectUnderTest.executeCmHandleIdSearchForInventory(conditionServiceProps)
214 then: 'the result returns the correct 2 elements'
215 assert result.size() == 2
216 assert result.contains('cmHandle1')
217 assert result.contains('cmHandle2')
220 def 'Get cm handle composite state'() {
221 given: 'a yang modelled cm handle'
222 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED,
223 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.LOCKED_MODULE_SYNC_FAILED).details("lock details").build(),
224 lastUpdateTime: 'some-timestamp',
225 dataSyncEnabled: false,
226 dataStores: dataStores())
227 def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
228 def publicProperties = [new YangModelCmHandle.Property('public prop', 'some public prop')]
229 def yangModelCmHandle = new YangModelCmHandle(id:'some-cm-handle', dmiServiceName: 'some service name', dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
230 and: 'the system returns this yang modelled cm handle'
231 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
232 when: 'getting cm handle composite state for a given cm handle id from ncmp service'
233 def result = objectUnderTest.getCmHandleCompositeState('some-cm-handle')
234 then: 'the result returns the correct data'
235 result == compositeState
238 def 'Update resource data for pass-through running from dmi using POST #scenario DMI properties.'() {
239 given: 'cpsDataService returns valid datanode'
240 mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
241 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
242 when: 'get resource data is called'
243 objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
244 'testResourceId', UPDATE,
245 '{some-json}', 'application/json')
246 then: 'DMI called with correct data'
247 1 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi('testCmHandle', 'testResourceId',
248 UPDATE, '{some-json}', 'application/json')
249 >> { new ResponseEntity<>(HttpStatus.OK) }
252 def 'Verify modules and create anchor params'() {
253 given: 'dmi plugin registration return created cm handles'
254 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'service1', dmiModelPlugin: 'service1',
255 dmiDataPlugin: 'service2')
256 dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
257 mockDmiPluginRegistration.getCreatedCmHandles() >> [ncmpServiceCmHandle]
258 when: 'parse and create cm handle in dmi registration then sync module'
259 objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(mockDmiPluginRegistration)
260 then: 'system persists the cm handle state'
261 1 * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(_) >> {
264 def cmHandleStatePerCmHandle = (args[0] as Map)
265 cmHandleStatePerCmHandle.each {
266 assert (it.key.id == 'test-cm-handle-id'
267 && it.value == CmHandleState.ADVISED)
273 def 'Execute cm handle id search'() {
274 given: 'valid CmHandleQueryApiParameters input'
275 def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
276 def conditionApiProperties = new ConditionApiProperties()
277 conditionApiProperties.conditionName = 'hasAllModules'
278 conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
279 cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
280 and: 'query cm handle method return with a data node list'
281 mockCpsCmHandlerQueryService.queryCmHandleIds(
282 spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
283 >> ['cm-handle-id-1']
284 when: 'execute cm handle search is called'
285 def result = objectUnderTest.executeCmHandleIdSearch(cmHandleQueryApiParameters)
286 then: 'result is the same collection as returned by the CPS Data Service'
287 assert result == ['cm-handle-id-1']
290 def 'Getting module definitions.'() {
291 when: 'get module definitions method is called with a valid cm handle ID'
292 objectUnderTest.getModuleDefinitionsByCmHandleId('some-cm-handle')
293 then: 'CPS module services is invoked once'
294 1 * mockInventoryPersistence.getModuleDefinitionsByCmHandleId('some-cm-handle')
297 def 'Execute cm handle search'() {
298 given: 'valid CmHandleQueryApiParameters input'
299 def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
300 def conditionApiProperties = new ConditionApiProperties()
301 conditionApiProperties.conditionName = 'hasAllModules'
302 conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
303 cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
304 and: 'query cm handle method return with a data node list'
305 mockCpsCmHandlerQueryService.queryCmHandles(
306 spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
307 >> [new NcmpServiceCmHandle(cmHandleId: 'cm-handle-id-1')]
308 when: 'execute cm handle search is called'
309 def result = objectUnderTest.executeCmHandleSearch(cmHandleQueryApiParameters)
310 then: 'result is the same collection as returned by the CPS Data Service'
311 assert result.stream().map(d -> d.cmHandleId).collect(Collectors.toSet()) == ['cm-handle-id-1'] as Set
314 def 'Set Cm Handle Data Sync Enabled Flag where data sync flag is #scenario'() {
315 given: 'an existing cm handle composite state'
316 def compositeState = new CompositeState(cmHandleState: CmHandleState.READY, dataSyncEnabled: initialDataSyncEnabledFlag,
317 dataStores: CompositeState.DataStores.builder()
318 .operationalDataStore(CompositeState.Operational.builder()
319 .dataStoreSyncState(initialDataSyncState)
321 and: 'get cm handle state returns the composite state for the given cm handle id'
322 mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
323 when: 'set data sync enabled is called with the data sync enabled flag set to #dataSyncEnabledFlag'
324 objectUnderTest.setDataSyncEnabled('some-cm-handle-id', dataSyncEnabledFlag)
325 then: 'the data sync enabled flag is set to #dataSyncEnabled'
326 compositeState.dataSyncEnabled == dataSyncEnabledFlag
327 and: 'the data store sync state is set to #expectedDataStoreSyncState'
328 compositeState.dataStores.operationalDataStore.dataStoreSyncState == expectedDataStoreSyncState
329 and: 'the cps data service to delete data nodes is invoked the expected number of times'
330 deleteDataNodeExpectedNumberOfInvocation * mockCpsDataService.deleteDataNode('NFP-Operational', 'some-cm-handle-id', '/netconf-state', _)
331 and: 'the inventory persistence service to update node leaves is called with the correct values'
332 saveCmHandleStateExpectedNumberOfInvocations * mockInventoryPersistence.saveCmHandleState('some-cm-handle-id', compositeState)
333 where: 'the following data sync enabled flag is used'
334 scenario | dataSyncEnabledFlag | initialDataSyncEnabledFlag | initialDataSyncState || expectedDataStoreSyncState | deleteDataNodeExpectedNumberOfInvocation | saveCmHandleStateExpectedNumberOfInvocations
335 'enabled' | true | false | DataStoreSyncState.NONE_REQUESTED || DataStoreSyncState.UNSYNCHRONIZED | 0 | 1
336 'disabled' | false | true | DataStoreSyncState.UNSYNCHRONIZED || DataStoreSyncState.NONE_REQUESTED | 0 | 1
337 'disabled where sync-state is currently SYNCHRONIZED' | false | true | DataStoreSyncState.SYNCHRONIZED || DataStoreSyncState.NONE_REQUESTED | 1 | 1
338 'is set to existing flag state' | true | true | DataStoreSyncState.UNSYNCHRONIZED || DataStoreSyncState.UNSYNCHRONIZED | 0 | 0
341 def 'Set cm Handle Data Sync Enabled flag with following cm handle not in ready state exception' () {
342 given: 'a cm handle composite state'
343 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED, dataSyncEnabled: false)
344 and: 'get cm handle state returns the composite state for the given cm handle id'
345 mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
346 when: 'set data sync enabled is called with the data sync enabled flag set to true'
347 objectUnderTest.setDataSyncEnabled('some-cm-handle-id', true)
348 then: 'the expected exception is thrown'
350 and: 'the inventory persistence service to update node leaves is not invoked'
351 0 * mockInventoryPersistence.saveCmHandleState(_, _)
354 def 'Get all cm handle IDs by DMI plugin identifier.' () {
355 given: 'cm handle queries service returns cm handles'
356 1 * mockCmHandleQueries.getCmHandleIdsByDmiPluginIdentifier('some-dmi-plugin-identifier') >> ['cm-handle-1','cm-handle-2']
357 when: 'cm handle Ids are requested with dmi plugin identifier'
358 def result = objectUnderTest.getAllCmHandleIdsByDmiPluginIdentifier('some-dmi-plugin-identifier')
359 then: 'the result size is correct'
360 assert result.size() == 2
361 and: 'the result returns the correct details'
362 assert result.containsAll('cm-handle-1','cm-handle-2')
366 CompositeState.DataStores.builder()
367 .operationalDataStore(CompositeState.Operational.builder()
368 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
369 .lastSyncTime('some-timestamp').build()).build()
373 mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
374 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode