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.DataOperationDefinition
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.DataOperationRequest
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.OPERATIONAL
58 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL
59 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING
60 import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE
61 import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE
63 class NetworkCmProxyDataServiceImplSpec extends Specification {
65 def mockCpsDataService = Mock(CpsDataService)
66 def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
67 def mockDmiDataOperations = Mock(DmiDataOperations)
68 def nullNetworkCmProxyDataServicePropertyHandler = null
69 def mockInventoryPersistence = Mock(InventoryPersistence)
70 def mockCmHandleQueries = Mock(CmHandleQueries)
71 def mockDmiPluginRegistration = Mock(DmiPluginRegistration)
72 def mockCpsCmHandlerQueryService = Mock(NetworkCmProxyCmHandleQueryService)
73 def mockLcmEventsCmHandleStateHandler = Mock(LcmEventsCmHandleStateHandler)
74 def stubModuleSyncStartedOnCmHandles = Stub(IMap<String, Object>)
77 def NO_REQUEST_ID = null
79 def OPTIONS_PARAM = '(a=1,b=2)'
81 def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'test-cm-handle-id')
83 def objectUnderTest = new NetworkCmProxyDataServiceImpl(
84 spiedJsonObjectMapper,
85 mockDmiDataOperations,
86 nullNetworkCmProxyDataServicePropertyHandler,
87 mockInventoryPersistence,
89 mockCpsCmHandlerQueryService,
90 mockLcmEventsCmHandleStateHandler,
92 stubModuleSyncStartedOnCmHandles)
94 def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
96 def dataNode = [new DataNode(leaves: ['id': 'some-cm-handle', 'dmi-service-name': 'testDmiService'])]
98 def 'Write resource data for pass-through running from DMI using POST.'() {
99 given: 'cpsDataService returns valid datanode'
101 when: 'write resource data is called'
102 objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
103 'testResourceId', CREATE,
104 '{some-json}', 'application/json')
105 then: 'DMI called with correct data'
106 1 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi('testCmHandle', 'testResourceId',
107 CREATE, '{some-json}', 'application/json')
108 >> { new ResponseEntity<>(HttpStatus.CREATED) }
111 def 'Get resource data for pass-through operational from DMI.'() {
112 given: 'cpsDataService returns valid data node'
114 and: 'get resource data from DMI is called'
115 mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_OPERATIONAL.datastoreName,'testCmHandle', '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', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID)
119 then: 'DMI returns a json response'
120 assert response == 'dmi-response'
123 def 'Get resource data for pass-through running from DMI.'() {
124 given: 'cpsDataService returns valid data node'
126 and: 'DMI returns valid response and data'
127 mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle', '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.datastoreName, 'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID)
131 then: 'get resource data returns expected response'
132 assert response == '{dmi-response}'
135 def 'Get resource data for operational (cached) data.'() {
136 given: 'CPS Data service returns some object(s)'
137 mockCpsDataService.getDataNodes(OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', FetchDescendantsOption.OMIT_DESCENDANTS) >> ['First Object', 'other Object']
138 when: 'get resource data is called'
139 def response = objectUnderTest.getResourceDataForCmHandle(OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', FetchDescendantsOption.OMIT_DESCENDANTS)
140 then: 'get resource data returns teh first object from the data service'
141 assert response == 'First Object'
144 def 'Execute (async) data operation for #datastoreName from DMI.'() {
145 given: 'cpsDataService returns valid data node'
146 def dataOperationRequest = getDataOperationRequest(datastoreName)
147 when: 'request resource data for data operation is called'
148 objectUnderTest.executeDataOperationForCmHandles('some topic', dataOperationRequest, 'requestId')
149 then: 'request resource data for data operation returns expected response'
150 1 * mockDmiDataOperations.requestResourceDataFromDmi('some topic', dataOperationRequest, 'requestId')
151 where: 'the following data stores are used'
152 datastoreName << [PASSTHROUGH_RUNNING.datastoreName, PASSTHROUGH_OPERATIONAL.datastoreName]
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(_) >> {
263 def cmHandleStatePerCmHandle = (args[0] as Map)
264 cmHandleStatePerCmHandle.each {
265 assert it.key.id == 'test-cm-handle-id' && it.value == CmHandleState.ADVISED
271 def 'Execute cm handle id search'() {
272 given: 'valid CmHandleQueryApiParameters input'
273 def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
274 def conditionApiProperties = new ConditionApiProperties()
275 conditionApiProperties.conditionName = 'hasAllModules'
276 conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
277 cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
278 and: 'query cm handle method return with a data node list'
279 mockCpsCmHandlerQueryService.queryCmHandleIds(
280 spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
281 >> ['cm-handle-id-1']
282 when: 'execute cm handle search is called'
283 def result = objectUnderTest.executeCmHandleIdSearch(cmHandleQueryApiParameters)
284 then: 'result is the same collection as returned by the CPS Data Service'
285 assert result == ['cm-handle-id-1']
288 def 'Getting module definitions.'() {
289 when: 'get module definitions method is called with a valid cm handle ID'
290 objectUnderTest.getModuleDefinitionsByCmHandleId('some-cm-handle')
291 then: 'CPS module services is invoked once'
292 1 * mockInventoryPersistence.getModuleDefinitionsByCmHandleId('some-cm-handle')
295 def 'Execute cm handle search'() {
296 given: 'valid CmHandleQueryApiParameters input'
297 def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
298 def conditionApiProperties = new ConditionApiProperties()
299 conditionApiProperties.conditionName = 'hasAllModules'
300 conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
301 cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
302 and: 'query cm handle method return with a data node list'
303 mockCpsCmHandlerQueryService.queryCmHandles(
304 spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
305 >> [new NcmpServiceCmHandle(cmHandleId: 'cm-handle-id-1')]
306 when: 'execute cm handle search is called'
307 def result = objectUnderTest.executeCmHandleSearch(cmHandleQueryApiParameters)
308 then: 'result is the same collection as returned by the CPS Data Service'
309 assert result.stream().map(d -> d.cmHandleId).collect(Collectors.toSet()) == ['cm-handle-id-1'] as Set
312 def 'Set Cm Handle Data Sync Enabled Flag where data sync flag is #scenario'() {
313 given: 'an existing cm handle composite state'
314 def compositeState = new CompositeState(cmHandleState: CmHandleState.READY, dataSyncEnabled: initialDataSyncEnabledFlag,
315 dataStores: CompositeState.DataStores.builder()
316 .operationalDataStore(CompositeState.Operational.builder()
317 .dataStoreSyncState(initialDataSyncState)
319 and: 'get cm handle state returns the composite state for the given cm handle id'
320 mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
321 when: 'set data sync enabled is called with the data sync enabled flag set to #dataSyncEnabledFlag'
322 objectUnderTest.setDataSyncEnabled('some-cm-handle-id', dataSyncEnabledFlag)
323 then: 'the data sync enabled flag is set to #dataSyncEnabled'
324 compositeState.dataSyncEnabled == dataSyncEnabledFlag
325 and: 'the data store sync state is set to #expectedDataStoreSyncState'
326 compositeState.dataStores.operationalDataStore.dataStoreSyncState == expectedDataStoreSyncState
327 and: 'the cps data service to delete data nodes is invoked the expected number of times'
328 deleteDataNodeExpectedNumberOfInvocation * mockCpsDataService.deleteDataNode('NFP-Operational', 'some-cm-handle-id', '/netconf-state', _)
329 and: 'the inventory persistence service to update node leaves is called with the correct values'
330 saveCmHandleStateExpectedNumberOfInvocations * mockInventoryPersistence.saveCmHandleState('some-cm-handle-id', compositeState)
331 where: 'the following data sync enabled flag is used'
332 scenario | dataSyncEnabledFlag | initialDataSyncEnabledFlag | initialDataSyncState || expectedDataStoreSyncState | deleteDataNodeExpectedNumberOfInvocation | saveCmHandleStateExpectedNumberOfInvocations
333 'enabled' | true | false | DataStoreSyncState.NONE_REQUESTED || DataStoreSyncState.UNSYNCHRONIZED | 0 | 1
334 'disabled' | false | true | DataStoreSyncState.UNSYNCHRONIZED || DataStoreSyncState.NONE_REQUESTED | 0 | 1
335 'disabled where sync-state is currently SYNCHRONIZED' | false | true | DataStoreSyncState.SYNCHRONIZED || DataStoreSyncState.NONE_REQUESTED | 1 | 1
336 'is set to existing flag state' | true | true | DataStoreSyncState.UNSYNCHRONIZED || DataStoreSyncState.UNSYNCHRONIZED | 0 | 0
339 def 'Set cm Handle Data Sync Enabled flag with following cm handle not in ready state exception' () {
340 given: 'a cm handle composite state'
341 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED, dataSyncEnabled: false)
342 and: 'get cm handle state returns the composite state for the given cm handle id'
343 mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
344 when: 'set data sync enabled is called with the data sync enabled flag set to true'
345 objectUnderTest.setDataSyncEnabled('some-cm-handle-id', true)
346 then: 'the expected exception is thrown'
348 and: 'the inventory persistence service to update node leaves is not invoked'
349 0 * mockInventoryPersistence.saveCmHandleState(_, _)
352 def 'Get all cm handle IDs by DMI plugin identifier.' () {
353 given: 'cm handle queries service returns cm handles'
354 1 * mockCmHandleQueries.getCmHandleIdsByDmiPluginIdentifier('some-dmi-plugin-identifier') >> ['cm-handle-1','cm-handle-2']
355 when: 'cm handle Ids are requested with dmi plugin identifier'
356 def result = objectUnderTest.getAllCmHandleIdsByDmiPluginIdentifier('some-dmi-plugin-identifier')
357 then: 'the result size is correct'
358 assert result.size() == 2
359 and: 'the result returns the correct details'
360 assert result.containsAll('cm-handle-1','cm-handle-2')
364 CompositeState.DataStores.builder()
365 .operationalDataStore(CompositeState.Operational.builder()
366 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
367 .lastSyncTime('some-timestamp').build()).build()
371 mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
372 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
375 def getDataOperationRequest(datastore) {
376 def dataOperationRequest = new DataOperationRequest()
377 def dataOperationDefinitions = new ArrayList()
378 dataOperationDefinitions.add(getDataOperationDefinition(datastore))
379 dataOperationRequest.setDataOperationDefinitions(dataOperationDefinitions)
380 return dataOperationRequest
383 def getDataOperationDefinition(datastore) {
384 def dataOperationDefinition = new DataOperationDefinition()
385 dataOperationDefinition.setOperation("read")
386 dataOperationDefinition.setOperationId("operational-12")
387 dataOperationDefinition.setDatastore(datastore)
388 def targetIds = new ArrayList()
389 targetIds.add("some-cm-handle")
390 dataOperationDefinition.setCmHandleIds(targetIds)
391 return dataOperationDefinition