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