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 static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
27 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
28 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
29 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.OPERATIONAL
30 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL
31 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING
32 import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE
33 import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE
35 import com.hazelcast.map.IMap
36 import org.onap.cps.ncmp.api.NetworkCmProxyCmHandleQueryService
37 import org.onap.cps.ncmp.api.impl.events.lcm.LcmEventsCmHandleStateHandler
38 import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevel
39 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
40 import org.onap.cps.ncmp.api.impl.inventory.CmHandleQueries
41 import org.onap.cps.ncmp.api.impl.inventory.CmHandleState
42 import org.onap.cps.ncmp.api.impl.inventory.CompositeState
43 import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence
44 import org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory
45 import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState
46 import org.onap.cps.ncmp.api.models.DataOperationDefinition
47 import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters
48 import org.onap.cps.ncmp.api.models.CmHandleQueryServiceParameters
49 import org.onap.cps.ncmp.api.models.ConditionApiProperties
50 import org.onap.cps.ncmp.api.models.DmiPluginRegistration
51 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
52 import org.onap.cps.ncmp.api.models.DataOperationRequest
53 import org.onap.cps.spi.exceptions.CpsException
54 import org.onap.cps.spi.model.ConditionProperties
55 import spock.lang.Shared
56 import java.util.stream.Collectors
57 import org.onap.cps.utils.JsonObjectMapper
58 import com.fasterxml.jackson.databind.ObjectMapper
59 import org.onap.cps.api.CpsDataService
60 import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
61 import org.onap.cps.spi.FetchDescendantsOption
62 import org.onap.cps.spi.model.DataNode
63 import org.springframework.http.HttpStatus
64 import org.springframework.http.ResponseEntity
65 import spock.lang.Specification
67 class NetworkCmProxyDataServiceImplSpec extends Specification {
69 def mockCpsDataService = Mock(CpsDataService)
70 def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
71 def mockDmiDataOperations = Mock(DmiDataOperations)
72 def nullNetworkCmProxyDataServicePropertyHandler = null
73 def mockInventoryPersistence = Mock(InventoryPersistence)
74 def mockCmHandleQueries = Mock(CmHandleQueries)
75 def mockDmiPluginRegistration = Mock(DmiPluginRegistration)
76 def mockCpsCmHandlerQueryService = Mock(NetworkCmProxyCmHandleQueryService)
77 def mockLcmEventsCmHandleStateHandler = Mock(LcmEventsCmHandleStateHandler)
78 def stubModuleSyncStartedOnCmHandles = Stub(IMap<String, Object>)
79 def stubTrustLevelPerDmiPlugin = Stub(IMap<String, TrustLevel>)
82 def NO_REQUEST_ID = null
84 def OPTIONS_PARAM = '(a=1,b=2)'
86 def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'test-cm-handle-id')
88 def objectUnderTest = new NetworkCmProxyDataServiceImpl(
89 spiedJsonObjectMapper,
90 mockDmiDataOperations,
91 nullNetworkCmProxyDataServicePropertyHandler,
92 mockInventoryPersistence,
94 mockCpsCmHandlerQueryService,
95 mockLcmEventsCmHandleStateHandler,
97 stubModuleSyncStartedOnCmHandles,
98 stubTrustLevelPerDmiPlugin)
100 def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
102 def dataNode = [new DataNode(leaves: ['id': 'some-cm-handle', 'dmi-service-name': 'testDmiService'])]
104 def 'Write resource data for pass-through running from DMI using POST.'() {
105 given: 'cpsDataService returns valid datanode'
107 when: 'write resource data is called'
108 objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
109 'testResourceId', CREATE,
110 '{some-json}', 'application/json')
111 then: 'DMI called with correct data'
112 1 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi('testCmHandle', 'testResourceId',
113 CREATE, '{some-json}', 'application/json')
114 >> { new ResponseEntity<>(HttpStatus.CREATED) }
117 def 'Get resource data for pass-through operational from DMI.'() {
118 given: 'cpsDataService returns valid data node'
120 and: 'get resource data from DMI is called'
121 mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_OPERATIONAL.datastoreName,'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID) >>
122 new ResponseEntity<>('dmi-response', HttpStatus.OK)
123 when: 'get resource data operational for cm-handle is called'
124 def response = objectUnderTest.getResourceDataForCmHandle(PASSTHROUGH_OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID)
125 then: 'DMI returns a json response'
126 assert response == 'dmi-response'
129 def 'Get resource data for pass-through running from DMI.'() {
130 given: 'cpsDataService returns valid data node'
132 and: 'DMI returns valid response and data'
133 mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID) >>
134 new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
135 when: 'get resource data is called'
136 def response = objectUnderTest.getResourceDataForCmHandle(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID)
137 then: 'get resource data returns expected response'
138 assert response == '{dmi-response}'
141 def 'Get resource data for operational (cached) data.'() {
142 given: 'CPS Data service returns some object(s)'
143 mockCpsDataService.getDataNodes(OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', FetchDescendantsOption.OMIT_DESCENDANTS) >> ['First Object', 'other Object']
144 when: 'get resource data is called'
145 def response = objectUnderTest.getResourceDataForCmHandle(OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', FetchDescendantsOption.OMIT_DESCENDANTS)
146 then: 'get resource data returns teh first object from the data service'
147 assert response == 'First Object'
150 def 'Execute (async) data operation for #datastoreName from DMI.'() {
151 given: 'cpsDataService returns valid data node'
152 def dataOperationRequest = getDataOperationRequest(datastoreName)
153 when: 'request resource data for data operation is called'
154 objectUnderTest.executeDataOperationForCmHandles('some topic', dataOperationRequest, 'requestId')
155 then: 'request resource data for data operation returns expected response'
156 1 * mockDmiDataOperations.requestResourceDataFromDmi('some topic', dataOperationRequest, 'requestId')
157 where: 'the following data stores are used'
158 datastoreName << [PASSTHROUGH_RUNNING.datastoreName, PASSTHROUGH_OPERATIONAL.datastoreName]
161 def 'Getting Yang Resources.'() {
162 when: 'yang resources is called'
163 objectUnderTest.getYangResourcesModuleReferences('some-cm-handle')
164 then: 'CPS module services is invoked for the correct dataspace and cm handle'
165 1 * mockInventoryPersistence.getYangResourcesModuleReferences('some-cm-handle')
168 def 'Get a cm handle.'() {
169 given: 'the system returns a yang modelled cm handle'
170 def dmiServiceName = 'some service name'
171 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED,
172 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.MODULE_SYNC_FAILED).details("lock details").build(),
173 lastUpdateTime: 'some-timestamp',
174 dataSyncEnabled: false,
175 dataStores: dataStores())
176 def dmiProperties = [new YangModelCmHandle.Property('Book', 'Romance Novel')]
177 def publicProperties = [new YangModelCmHandle.Property('Public Book', 'Public Romance Novel')]
178 def yangModelCmHandle = new YangModelCmHandle(id: 'some-cm-handle', dmiServiceName: dmiServiceName,
179 dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
180 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
181 when: 'getting cm handle details for a given cm handle id from ncmp service'
182 def result = objectUnderTest.getNcmpServiceCmHandle('some-cm-handle')
183 then: 'the result is a ncmpServiceCmHandle'
184 result.class == NcmpServiceCmHandle.class
185 and: 'the cm handle contains the cm handle id'
186 result.cmHandleId == 'some-cm-handle'
187 and: 'the cm handle contains the DMI Properties'
188 result.dmiProperties ==[ Book:'Romance Novel' ]
189 and: 'the cm handle contains the public Properties'
190 result.publicProperties == [ "Public Book":'Public Romance Novel' ]
191 and: 'the cm handle contains the cm handle composite state'
192 result.compositeState == compositeState
196 def 'Get cm handle public properties'() {
197 given: 'a yang modelled cm handle'
198 def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
199 def publicProperties = [new YangModelCmHandle.Property('public prop', 'some public prop')]
200 def yangModelCmHandle = new YangModelCmHandle(id:'some-cm-handle', dmiServiceName: 'some service name', dmiProperties: dmiProperties, publicProperties: publicProperties)
201 and: 'the system returns this yang modelled cm handle'
202 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
203 when: 'getting cm handle public properties for a given cm handle id from ncmp service'
204 def result = objectUnderTest.getCmHandlePublicProperties('some-cm-handle')
205 then: 'the result returns the correct data'
206 result == [ 'public prop' : 'some public prop' ]
209 def 'Execute cm handle id search for inventory'() {
210 given: 'a ConditionApiProperties object'
211 def conditionProperties = new ConditionProperties()
212 conditionProperties.conditionName = 'hasAllProperties'
213 conditionProperties.conditionParameters = [ [ 'some-key' : 'some-value' ] ]
214 def conditionServiceProps = new CmHandleQueryServiceParameters()
215 conditionServiceProps.cmHandleQueryParameters = [conditionProperties] as List<ConditionProperties>
216 and: 'the system returns an set of cmHandle ids'
217 mockCpsCmHandlerQueryService.queryCmHandleIdsForInventory(*_) >> [ 'cmHandle1', 'cmHandle2' ]
218 when: 'getting cm handle id set for a given dmi property'
219 def result = objectUnderTest.executeCmHandleIdSearchForInventory(conditionServiceProps)
220 then: 'the result returns the correct 2 elements'
221 assert result.size() == 2
222 assert result.contains('cmHandle1')
223 assert result.contains('cmHandle2')
226 def 'Get cm handle composite state'() {
227 given: 'a yang modelled cm handle'
228 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED,
229 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.MODULE_SYNC_FAILED).details("lock details").build(),
230 lastUpdateTime: 'some-timestamp',
231 dataSyncEnabled: false,
232 dataStores: dataStores())
233 def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
234 def publicProperties = [new YangModelCmHandle.Property('public prop', 'some public prop')]
235 def yangModelCmHandle = new YangModelCmHandle(id:'some-cm-handle', dmiServiceName: 'some service name', dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
236 and: 'the system returns this yang modelled cm handle'
237 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
238 when: 'getting cm handle composite state for a given cm handle id from ncmp service'
239 def result = objectUnderTest.getCmHandleCompositeState('some-cm-handle')
240 then: 'the result returns the correct data'
241 result == compositeState
244 def 'Update resource data for pass-through running from dmi using POST #scenario DMI properties.'() {
245 given: 'cpsDataService returns valid datanode'
246 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
247 when: 'get resource data is called'
248 objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
249 'testResourceId', UPDATE,
250 '{some-json}', 'application/json')
251 then: 'DMI called with correct data'
252 1 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi('testCmHandle', 'testResourceId',
253 UPDATE, '{some-json}', 'application/json')
254 >> { new ResponseEntity<>(HttpStatus.OK) }
257 def 'Verify modules and create anchor params.'() {
258 given: 'dmi plugin registration return created cm handles'
259 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'service1', dmiModelPlugin: 'service1',
260 dmiDataPlugin: 'service2')
261 dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
262 mockDmiPluginRegistration.getCreatedCmHandles() >> [ncmpServiceCmHandle]
263 when: 'parse and create cm handle in dmi registration then sync module'
264 objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(mockDmiPluginRegistration)
265 then: 'system persists the cm handle state'
266 1 * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(_) >> {
268 def cmHandleStatePerCmHandle = (args[0] as Map)
269 cmHandleStatePerCmHandle.each {
270 assert it.key.id == 'test-cm-handle-id' && it.value == CmHandleState.ADVISED
276 def 'Execute cm handle id search'() {
277 given: 'valid CmHandleQueryApiParameters input'
278 def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
279 def conditionApiProperties = new ConditionApiProperties()
280 conditionApiProperties.conditionName = 'hasAllModules'
281 conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
282 cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
283 and: 'query cm handle method return with a data node list'
284 mockCpsCmHandlerQueryService.queryCmHandleIds(
285 spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
286 >> ['cm-handle-id-1']
287 when: 'execute cm handle search is called'
288 def result = objectUnderTest.executeCmHandleIdSearch(cmHandleQueryApiParameters)
289 then: 'result is the same collection as returned by the CPS Data Service'
290 assert result == ['cm-handle-id-1']
293 def 'Getting module definitions.'() {
294 when: 'get module definitions method is called with a valid cm handle ID'
295 objectUnderTest.getModuleDefinitionsByCmHandleId('some-cm-handle')
296 then: 'CPS module services is invoked once'
297 1 * mockInventoryPersistence.getModuleDefinitionsByCmHandleId('some-cm-handle')
300 def 'Execute cm handle search'() {
301 given: 'valid CmHandleQueryApiParameters input'
302 def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
303 def conditionApiProperties = new ConditionApiProperties()
304 conditionApiProperties.conditionName = 'hasAllModules'
305 conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
306 cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
307 and: 'query cm handle method return with a data node list'
308 mockCpsCmHandlerQueryService.queryCmHandles(
309 spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
310 >> [new NcmpServiceCmHandle(cmHandleId: 'cm-handle-id-1')]
311 when: 'execute cm handle search is called'
312 def result = objectUnderTest.executeCmHandleSearch(cmHandleQueryApiParameters)
313 then: 'result is the same collection as returned by the CPS Data Service'
314 assert result.stream().map(d -> d.cmHandleId).collect(Collectors.toSet()) == ['cm-handle-id-1'] as Set
317 def 'Set Cm Handle Data Sync Enabled Flag where data sync flag is #scenario'() {
318 given: 'an existing cm handle composite state'
319 def compositeState = new CompositeState(cmHandleState: CmHandleState.READY, dataSyncEnabled: initialDataSyncEnabledFlag,
320 dataStores: CompositeState.DataStores.builder()
321 .operationalDataStore(CompositeState.Operational.builder()
322 .dataStoreSyncState(initialDataSyncState)
324 and: 'get cm handle state returns the composite state for the given cm handle id'
325 mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
326 when: 'set data sync enabled is called with the data sync enabled flag set to #dataSyncEnabledFlag'
327 objectUnderTest.setDataSyncEnabled('some-cm-handle-id', dataSyncEnabledFlag)
328 then: 'the data sync enabled flag is set to #dataSyncEnabled'
329 compositeState.dataSyncEnabled == dataSyncEnabledFlag
330 and: 'the data store sync state is set to #expectedDataStoreSyncState'
331 compositeState.dataStores.operationalDataStore.dataStoreSyncState == expectedDataStoreSyncState
332 and: 'the cps data service to delete data nodes is invoked the expected number of times'
333 deleteDataNodeExpectedNumberOfInvocation * mockCpsDataService.deleteDataNode(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'some-cm-handle-id', '/netconf-state', _)
334 and: 'the inventory persistence service to update node leaves is called with the correct values'
335 saveCmHandleStateExpectedNumberOfInvocations * mockInventoryPersistence.saveCmHandleState('some-cm-handle-id', compositeState)
336 where: 'the following data sync enabled flag is used'
337 scenario | dataSyncEnabledFlag | initialDataSyncEnabledFlag | initialDataSyncState || expectedDataStoreSyncState | deleteDataNodeExpectedNumberOfInvocation | saveCmHandleStateExpectedNumberOfInvocations
338 'enabled' | true | false | DataStoreSyncState.NONE_REQUESTED || DataStoreSyncState.UNSYNCHRONIZED | 0 | 1
339 'disabled' | false | true | DataStoreSyncState.UNSYNCHRONIZED || DataStoreSyncState.NONE_REQUESTED | 0 | 1
340 'disabled where sync-state is currently SYNCHRONIZED' | false | true | DataStoreSyncState.SYNCHRONIZED || DataStoreSyncState.NONE_REQUESTED | 1 | 1
341 'is set to existing flag state' | true | true | DataStoreSyncState.UNSYNCHRONIZED || DataStoreSyncState.UNSYNCHRONIZED | 0 | 0
344 def 'Set cm Handle Data Sync Enabled flag with following cm handle not in ready state exception' () {
345 given: 'a cm handle composite state'
346 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED, dataSyncEnabled: false)
347 and: 'get cm handle state returns the composite state for the given cm handle id'
348 mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
349 when: 'set data sync enabled is called with the data sync enabled flag set to true'
350 objectUnderTest.setDataSyncEnabled('some-cm-handle-id', true)
351 then: 'the expected exception is thrown'
353 and: 'the inventory persistence service to update node leaves is not invoked'
354 0 * mockInventoryPersistence.saveCmHandleState(_, _)
357 def 'Get all cm handle IDs by DMI plugin identifier.' () {
358 given: 'cm handle queries service returns cm handles'
359 1 * mockCmHandleQueries.getCmHandleIdsByDmiPluginIdentifier('some-dmi-plugin-identifier') >> ['cm-handle-1','cm-handle-2']
360 when: 'cm handle Ids are requested with dmi plugin identifier'
361 def result = objectUnderTest.getAllCmHandleIdsByDmiPluginIdentifier('some-dmi-plugin-identifier')
362 then: 'the result size is correct'
363 assert result.size() == 2
364 and: 'the result returns the correct details'
365 assert result.containsAll('cm-handle-1','cm-handle-2')
369 CompositeState.DataStores.builder()
370 .operationalDataStore(CompositeState.Operational.builder()
371 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
372 .lastSyncTime('some-timestamp').build()).build()
376 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
379 def getDataOperationRequest(datastore) {
380 def dataOperationRequest = new DataOperationRequest()
381 def dataOperationDefinitions = new ArrayList()
382 dataOperationDefinitions.add(getDataOperationDefinition(datastore))
383 dataOperationRequest.setDataOperationDefinitions(dataOperationDefinitions)
384 return dataOperationRequest
387 def getDataOperationDefinition(datastore) {
388 def dataOperationDefinition = new DataOperationDefinition()
389 dataOperationDefinition.setOperation("read")
390 dataOperationDefinition.setOperationId("operational-12")
391 dataOperationDefinition.setDatastore(datastore)
392 def targetIds = new ArrayList()
393 targetIds.add("some-cm-handle")
394 dataOperationDefinition.setCmHandleIds(targetIds)
395 return dataOperationDefinition