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.BatchOperationDefinition
37 import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters
38 import org.onap.cps.ncmp.api.models.CmHandleQueryServiceParameters
39 import org.onap.cps.ncmp.api.models.ConditionApiProperties
40 import org.onap.cps.ncmp.api.models.DmiPluginRegistration
41 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
42 import org.onap.cps.ncmp.api.models.ResourceDataBatchRequest
43 import org.onap.cps.spi.exceptions.CpsException
44 import org.onap.cps.spi.model.ConditionProperties
45 import spock.lang.Shared
46 import java.util.stream.Collectors
47 import org.onap.cps.utils.JsonObjectMapper
48 import com.fasterxml.jackson.databind.ObjectMapper
49 import org.onap.cps.api.CpsDataService
50 import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
51 import org.onap.cps.spi.FetchDescendantsOption
52 import org.onap.cps.spi.model.DataNode
53 import org.springframework.http.HttpStatus
54 import org.springframework.http.ResponseEntity
55 import spock.lang.Specification
57 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL
58 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING
59 import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE
60 import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE
62 class NetworkCmProxyDataServiceImplSpec extends Specification {
64 def mockCpsDataService = Mock(CpsDataService)
65 def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
66 def mockDmiDataOperations = Mock(DmiDataOperations)
67 def nullNetworkCmProxyDataServicePropertyHandler = null
68 def mockInventoryPersistence = Mock(InventoryPersistence)
69 def mockCmHandleQueries = Mock(CmHandleQueries)
70 def mockDmiPluginRegistration = Mock(DmiPluginRegistration)
71 def mockCpsCmHandlerQueryService = Mock(NetworkCmProxyCmHandleQueryService)
72 def mockLcmEventsCmHandleStateHandler = Mock(LcmEventsCmHandleStateHandler)
73 def stubModuleSyncStartedOnCmHandles = Stub(IMap<String, Object>)
76 def NO_REQUEST_ID = null
78 def OPTIONS_PARAM = '(a=1,b=2)'
80 def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'test-cm-handle-id')
82 def objectUnderTest = new NetworkCmProxyDataServiceImpl(
83 spiedJsonObjectMapper,
84 mockDmiDataOperations,
85 nullNetworkCmProxyDataServicePropertyHandler,
86 mockInventoryPersistence,
88 mockCpsCmHandlerQueryService,
89 mockLcmEventsCmHandleStateHandler,
91 stubModuleSyncStartedOnCmHandles)
93 def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
95 def dataNode = [new DataNode(leaves: ['id': 'some-cm-handle', 'dmi-service-name': 'testDmiService'])]
97 def 'Write resource data for pass-through running from DMI using POST.'() {
98 given: 'cpsDataService returns valid datanode'
100 when: 'write resource data is called'
101 objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
102 'testResourceId', CREATE,
103 '{some-json}', 'application/json')
104 then: 'DMI called with correct data'
105 1 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi('testCmHandle', 'testResourceId',
106 CREATE, '{some-json}', 'application/json')
107 >> { new ResponseEntity<>(HttpStatus.CREATED) }
110 def 'Get resource data for pass-through operational from DMI.'() {
111 given: 'get data node is called'
113 and: 'get resource data from DMI is called'
114 mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_OPERATIONAL.datastoreName,'testCmHandle',
115 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID) >>
116 new ResponseEntity<>('dmi-response', HttpStatus.OK)
117 when: 'get resource data operational for cm-handle is called'
118 def response = objectUnderTest.getResourceDataForCmHandle(PASSTHROUGH_OPERATIONAL.datastoreName, 'testCmHandle',
119 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID)
120 then: 'DMI returns a json response'
121 response == 'dmi-response'
124 def 'Get resource data for pass-through running from DMI.'() {
125 given: 'cpsDataService returns valid data node'
127 and: 'DMI returns valid response and data'
128 mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle',
129 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID) >>
130 new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
131 when: 'get resource data is called'
132 def response = objectUnderTest.getResourceDataForCmHandle(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle',
133 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID)
134 then: 'get resource data returns expected response'
135 response == '{dmi-response}'
138 def 'Get batch resource data for #datastoreName from DMI.'() {
139 given: 'cpsDataService returns valid data node'
140 def resourceDataBatchRequest = getResourceDataBatchRequest(datastoreName)
141 when: 'get batch resource data is called'
142 objectUnderTest.requestResourceDataForCmHandleBatch('some topic', resourceDataBatchRequest, 'requestId')
143 then: 'get batch resource data returns expected response'
144 1 * mockDmiDataOperations.requestResourceDataFromDmi('some topic', resourceDataBatchRequest, 'requestId')
145 where: 'the following data stores are used'
146 datastoreName << [PASSTHROUGH_RUNNING.datastoreName, PASSTHROUGH_OPERATIONAL.datastoreName]
149 def 'Getting Yang Resources.'() {
150 when: 'yang resources is called'
151 objectUnderTest.getYangResourcesModuleReferences('some-cm-handle')
152 then: 'CPS module services is invoked for the correct dataspace and cm handle'
153 1 * mockInventoryPersistence.getYangResourcesModuleReferences('some-cm-handle')
156 def 'Get a cm handle.'() {
157 given: 'the system returns a yang modelled cm handle'
158 def dmiServiceName = 'some service name'
159 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED,
160 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.LOCKED_MODULE_SYNC_FAILED).details("lock details").build(),
161 lastUpdateTime: 'some-timestamp',
162 dataSyncEnabled: false,
163 dataStores: dataStores())
164 def dmiProperties = [new YangModelCmHandle.Property('Book', 'Romance Novel')]
165 def publicProperties = [new YangModelCmHandle.Property('Public Book', 'Public Romance Novel')]
166 def yangModelCmHandle = new YangModelCmHandle(id: 'some-cm-handle', dmiServiceName: dmiServiceName,
167 dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
168 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
169 when: 'getting cm handle details for a given cm handle id from ncmp service'
170 def result = objectUnderTest.getNcmpServiceCmHandle('some-cm-handle')
171 then: 'the result is a ncmpServiceCmHandle'
172 result.class == NcmpServiceCmHandle.class
173 and: 'the cm handle contains the cm handle id'
174 result.cmHandleId == 'some-cm-handle'
175 and: 'the cm handle contains the DMI Properties'
176 result.dmiProperties ==[ Book:'Romance Novel' ]
177 and: 'the cm handle contains the public Properties'
178 result.publicProperties == [ "Public Book":'Public Romance Novel' ]
179 and: 'the cm handle contains the cm handle composite state'
180 result.compositeState == compositeState
184 def 'Get cm handle public properties'() {
185 given: 'a yang modelled cm handle'
186 def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
187 def publicProperties = [new YangModelCmHandle.Property('public prop', 'some public prop')]
188 def yangModelCmHandle = new YangModelCmHandle(id:'some-cm-handle', dmiServiceName: 'some service name', dmiProperties: dmiProperties, publicProperties: publicProperties)
189 and: 'the system returns this yang modelled cm handle'
190 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
191 when: 'getting cm handle public properties for a given cm handle id from ncmp service'
192 def result = objectUnderTest.getCmHandlePublicProperties('some-cm-handle')
193 then: 'the result returns the correct data'
194 result == [ 'public prop' : 'some public prop' ]
197 def 'Execute cm handle id search for inventory'() {
198 given: 'a ConditionApiProperties object'
199 def conditionProperties = new ConditionProperties()
200 conditionProperties.conditionName = 'hasAllProperties'
201 conditionProperties.conditionParameters = [ [ 'some-key' : 'some-value' ] ]
202 def conditionServiceProps = new CmHandleQueryServiceParameters()
203 conditionServiceProps.cmHandleQueryParameters = [conditionProperties] as List<ConditionProperties>
204 and: 'the system returns an set of cmHandle ids'
205 mockCpsCmHandlerQueryService.queryCmHandleIdsForInventory(*_) >> [ 'cmHandle1', 'cmHandle2' ]
206 when: 'getting cm handle id set for a given dmi property'
207 def result = objectUnderTest.executeCmHandleIdSearchForInventory(conditionServiceProps)
208 then: 'the result returns the correct 2 elements'
209 assert result.size() == 2
210 assert result.contains('cmHandle1')
211 assert result.contains('cmHandle2')
214 def 'Get cm handle composite state'() {
215 given: 'a yang modelled cm handle'
216 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED,
217 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.LOCKED_MODULE_SYNC_FAILED).details("lock details").build(),
218 lastUpdateTime: 'some-timestamp',
219 dataSyncEnabled: false,
220 dataStores: dataStores())
221 def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
222 def publicProperties = [new YangModelCmHandle.Property('public prop', 'some public prop')]
223 def yangModelCmHandle = new YangModelCmHandle(id:'some-cm-handle', dmiServiceName: 'some service name', dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
224 and: 'the system returns this yang modelled cm handle'
225 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
226 when: 'getting cm handle composite state for a given cm handle id from ncmp service'
227 def result = objectUnderTest.getCmHandleCompositeState('some-cm-handle')
228 then: 'the result returns the correct data'
229 result == compositeState
232 def 'Update resource data for pass-through running from dmi using POST #scenario DMI properties.'() {
233 given: 'cpsDataService returns valid datanode'
234 mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
235 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
236 when: 'get resource data is called'
237 objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
238 'testResourceId', UPDATE,
239 '{some-json}', 'application/json')
240 then: 'DMI called with correct data'
241 1 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi('testCmHandle', 'testResourceId',
242 UPDATE, '{some-json}', 'application/json')
243 >> { new ResponseEntity<>(HttpStatus.OK) }
246 def 'Verify modules and create anchor params'() {
247 given: 'dmi plugin registration return created cm handles'
248 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'service1', dmiModelPlugin: 'service1',
249 dmiDataPlugin: 'service2')
250 dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
251 mockDmiPluginRegistration.getCreatedCmHandles() >> [ncmpServiceCmHandle]
252 when: 'parse and create cm handle in dmi registration then sync module'
253 objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(mockDmiPluginRegistration)
254 then: 'system persists the cm handle state'
255 1 * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(_) >> {
258 def cmHandleStatePerCmHandle = (args[0] as Map)
259 cmHandleStatePerCmHandle.each {
260 assert (it.key.id == 'test-cm-handle-id'
261 && it.value == CmHandleState.ADVISED)
267 def 'Execute cm handle id search'() {
268 given: 'valid CmHandleQueryApiParameters input'
269 def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
270 def conditionApiProperties = new ConditionApiProperties()
271 conditionApiProperties.conditionName = 'hasAllModules'
272 conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
273 cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
274 and: 'query cm handle method return with a data node list'
275 mockCpsCmHandlerQueryService.queryCmHandleIds(
276 spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
277 >> ['cm-handle-id-1']
278 when: 'execute cm handle search is called'
279 def result = objectUnderTest.executeCmHandleIdSearch(cmHandleQueryApiParameters)
280 then: 'result is the same collection as returned by the CPS Data Service'
281 assert result == ['cm-handle-id-1']
284 def 'Getting module definitions.'() {
285 when: 'get module definitions method is called with a valid cm handle ID'
286 objectUnderTest.getModuleDefinitionsByCmHandleId('some-cm-handle')
287 then: 'CPS module services is invoked once'
288 1 * mockInventoryPersistence.getModuleDefinitionsByCmHandleId('some-cm-handle')
291 def 'Execute cm handle search'() {
292 given: 'valid CmHandleQueryApiParameters input'
293 def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
294 def conditionApiProperties = new ConditionApiProperties()
295 conditionApiProperties.conditionName = 'hasAllModules'
296 conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
297 cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
298 and: 'query cm handle method return with a data node list'
299 mockCpsCmHandlerQueryService.queryCmHandles(
300 spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
301 >> [new NcmpServiceCmHandle(cmHandleId: 'cm-handle-id-1')]
302 when: 'execute cm handle search is called'
303 def result = objectUnderTest.executeCmHandleSearch(cmHandleQueryApiParameters)
304 then: 'result is the same collection as returned by the CPS Data Service'
305 assert result.stream().map(d -> d.cmHandleId).collect(Collectors.toSet()) == ['cm-handle-id-1'] as Set
308 def 'Set Cm Handle Data Sync Enabled Flag where data sync flag is #scenario'() {
309 given: 'an existing cm handle composite state'
310 def compositeState = new CompositeState(cmHandleState: CmHandleState.READY, dataSyncEnabled: initialDataSyncEnabledFlag,
311 dataStores: CompositeState.DataStores.builder()
312 .operationalDataStore(CompositeState.Operational.builder()
313 .dataStoreSyncState(initialDataSyncState)
315 and: 'get cm handle state returns the composite state for the given cm handle id'
316 mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
317 when: 'set data sync enabled is called with the data sync enabled flag set to #dataSyncEnabledFlag'
318 objectUnderTest.setDataSyncEnabled('some-cm-handle-id', dataSyncEnabledFlag)
319 then: 'the data sync enabled flag is set to #dataSyncEnabled'
320 compositeState.dataSyncEnabled == dataSyncEnabledFlag
321 and: 'the data store sync state is set to #expectedDataStoreSyncState'
322 compositeState.dataStores.operationalDataStore.dataStoreSyncState == expectedDataStoreSyncState
323 and: 'the cps data service to delete data nodes is invoked the expected number of times'
324 deleteDataNodeExpectedNumberOfInvocation * mockCpsDataService.deleteDataNode('NFP-Operational', 'some-cm-handle-id', '/netconf-state', _)
325 and: 'the inventory persistence service to update node leaves is called with the correct values'
326 saveCmHandleStateExpectedNumberOfInvocations * mockInventoryPersistence.saveCmHandleState('some-cm-handle-id', compositeState)
327 where: 'the following data sync enabled flag is used'
328 scenario | dataSyncEnabledFlag | initialDataSyncEnabledFlag | initialDataSyncState || expectedDataStoreSyncState | deleteDataNodeExpectedNumberOfInvocation | saveCmHandleStateExpectedNumberOfInvocations
329 'enabled' | true | false | DataStoreSyncState.NONE_REQUESTED || DataStoreSyncState.UNSYNCHRONIZED | 0 | 1
330 'disabled' | false | true | DataStoreSyncState.UNSYNCHRONIZED || DataStoreSyncState.NONE_REQUESTED | 0 | 1
331 'disabled where sync-state is currently SYNCHRONIZED' | false | true | DataStoreSyncState.SYNCHRONIZED || DataStoreSyncState.NONE_REQUESTED | 1 | 1
332 'is set to existing flag state' | true | true | DataStoreSyncState.UNSYNCHRONIZED || DataStoreSyncState.UNSYNCHRONIZED | 0 | 0
335 def 'Set cm Handle Data Sync Enabled flag with following cm handle not in ready state exception' () {
336 given: 'a cm handle composite state'
337 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED, dataSyncEnabled: false)
338 and: 'get cm handle state returns the composite state for the given cm handle id'
339 mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
340 when: 'set data sync enabled is called with the data sync enabled flag set to true'
341 objectUnderTest.setDataSyncEnabled('some-cm-handle-id', true)
342 then: 'the expected exception is thrown'
344 and: 'the inventory persistence service to update node leaves is not invoked'
345 0 * mockInventoryPersistence.saveCmHandleState(_, _)
348 def 'Get all cm handle IDs by DMI plugin identifier.' () {
349 given: 'cm handle queries service returns cm handles'
350 1 * mockCmHandleQueries.getCmHandleIdsByDmiPluginIdentifier('some-dmi-plugin-identifier') >> ['cm-handle-1','cm-handle-2']
351 when: 'cm handle Ids are requested with dmi plugin identifier'
352 def result = objectUnderTest.getAllCmHandleIdsByDmiPluginIdentifier('some-dmi-plugin-identifier')
353 then: 'the result size is correct'
354 assert result.size() == 2
355 and: 'the result returns the correct details'
356 assert result.containsAll('cm-handle-1','cm-handle-2')
360 CompositeState.DataStores.builder()
361 .operationalDataStore(CompositeState.Operational.builder()
362 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
363 .lastSyncTime('some-timestamp').build()).build()
367 mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
368 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
371 def getResourceDataBatchRequest(datastore) {
372 def resourceDataBatchRequest = new ResourceDataBatchRequest()
373 def batchOperationDefinitions = new ArrayList()
374 batchOperationDefinitions.add(getBatchOperationDefinition(datastore))
375 resourceDataBatchRequest.setBatchOperationDefinitions(batchOperationDefinitions)
378 def getBatchOperationDefinition(datastore) {
379 def batchOperationDefinition = new BatchOperationDefinition()
380 batchOperationDefinition.setOperation("read")
381 batchOperationDefinition.setOperationId("operational-12")
382 batchOperationDefinition.setDatastore(datastore)
383 def targetIds = new ArrayList()
384 targetIds.add("some-cm-handle")
385 batchOperationDefinition.setCmHandleIds(targetIds)
386 return batchOperationDefinition