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.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 'Execute (async) data operation for #datastoreName from DMI.'() {
139 given: 'cpsDataService returns valid data node'
140 def dataOperationRequest = getDataOperationRequest(datastoreName)
141 when: 'request resource data for data operation is called'
142 objectUnderTest.executeDataOperationForCmHandles('some topic', dataOperationRequest, 'requestId')
143 then: 'request resource data for data operation returns expected response'
144 1 * mockDmiDataOperations.requestResourceDataFromDmi('some topic', dataOperationRequest, '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 getDataOperationRequest(datastore) {
372 def dataOperationRequest = new DataOperationRequest()
373 def dataOperationDefinitions = new ArrayList()
374 dataOperationDefinitions.add(getDataOperationDefinition(datastore))
375 dataOperationRequest.setDataOperationDefinitions(dataOperationDefinitions)
376 return dataOperationRequest
379 def getDataOperationDefinition(datastore) {
380 def dataOperationDefinition = new DataOperationDefinition()
381 dataOperationDefinition.setOperation("read")
382 dataOperationDefinition.setOperationId("operational-12")
383 dataOperationDefinition.setDatastore(datastore)
384 def targetIds = new ArrayList()
385 targetIds.add("some-cm-handle")
386 dataOperationDefinition.setCmHandleIds(targetIds)
387 return dataOperationDefinition