2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2021-2022 Nordix Foundation
4 * Modifications Copyright (C) 2021 Pantheon.tech
5 * Modifications Copyright (C) 2021-2022 Bell Canada
6 * ================================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
19 * SPDX-License-Identifier: Apache-2.0
20 * ============LICENSE_END=========================================================
23 package org.onap.cps.ncmp.api.impl
25 import org.onap.cps.ncmp.api.NetworkCmProxyCmHandlerQueryService
26 import org.onap.cps.ncmp.api.impl.event.lcm.LcmEventsCmHandleStateHandler
27 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
28 import org.onap.cps.ncmp.api.inventory.CmHandleState
29 import org.onap.cps.ncmp.api.inventory.CompositeState
30 import org.onap.cps.ncmp.api.inventory.InventoryPersistence
31 import org.onap.cps.ncmp.api.inventory.LockReasonCategory
32 import org.onap.cps.ncmp.api.inventory.DataStoreSyncState
33 import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters
34 import org.onap.cps.ncmp.api.models.ConditionApiProperties
35 import org.onap.cps.ncmp.api.models.DmiPluginRegistration
36 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
37 import org.onap.cps.spi.exceptions.CpsException
38 import org.onap.cps.spi.exceptions.DataValidationException
39 import org.onap.cps.spi.model.CmHandleQueryServiceParameters
40 import spock.lang.Shared
42 import java.util.stream.Collectors
44 import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL
45 import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING
46 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.CREATE
47 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.UPDATE
49 import org.onap.cps.utils.JsonObjectMapper
50 import com.fasterxml.jackson.databind.ObjectMapper
51 import org.onap.cps.api.CpsDataService
52 import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
53 import org.onap.cps.spi.FetchDescendantsOption
54 import org.onap.cps.spi.model.DataNode
55 import org.springframework.http.HttpStatus
56 import org.springframework.http.ResponseEntity
57 import spock.lang.Specification
59 class NetworkCmProxyDataServiceImplSpec extends Specification {
61 def mockCpsDataService = Mock(CpsDataService)
62 def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
63 def mockDmiDataOperations = Mock(DmiDataOperations)
64 def nullNetworkCmProxyDataServicePropertyHandler = null
65 def mockInventoryPersistence = Mock(InventoryPersistence)
66 def mockDmiPluginRegistration = Mock(DmiPluginRegistration)
67 def mockCpsCmHandlerQueryService = Mock(NetworkCmProxyCmHandlerQueryService)
68 def mockLcmEventsCmHandleStateHandler = Mock(LcmEventsCmHandleStateHandler)
71 def NO_REQUEST_ID = null
73 def OPTIONS_PARAM = '(a=1,b=2)'
75 def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'test-cm-handle-id')
77 def objectUnderTest = new NetworkCmProxyDataServiceImpl(
78 spiedJsonObjectMapper,
79 mockDmiDataOperations,
80 nullNetworkCmProxyDataServicePropertyHandler,
81 mockInventoryPersistence,
82 mockCpsCmHandlerQueryService,
83 mockLcmEventsCmHandleStateHandler,
86 def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
88 def dataNode = new DataNode(leaves: ['id': 'some-cm-handle', 'dmi-service-name': 'testDmiService'])
90 def 'Write resource data for pass-through running from DMI using POST.'() {
91 given: 'cpsDataService returns valid datanode'
92 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
93 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
94 when: 'write resource data is called'
95 objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
96 'testResourceId', CREATE,
97 '{some-json}', 'application/json')
98 then: 'DMI called with correct data'
99 1 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi('testCmHandle', 'testResourceId',
100 CREATE, '{some-json}', 'application/json')
101 >> { new ResponseEntity<>(HttpStatus.CREATED) }
104 def 'Write resource data for pass-through running from DMI using an invalid id.'() {
105 when: 'write resource data is called'
106 objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('invalid cm handle name',
107 'testResourceId', CREATE,
108 '{some-json}', 'application/json')
109 then: 'exception is thrown'
110 thrown(DataValidationException.class)
111 and: 'DMI is not invoked'
112 0 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi(_, _, _, _, _)
115 def 'Get resource data for pass-through operational from DMI.'() {
116 given: 'get data node is called'
117 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
118 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
119 and: 'get resource data from DMI is called'
120 mockDmiDataOperations.getResourceDataFromDmi(
124 PASSTHROUGH_OPERATIONAL,
126 NO_TOPIC) >> new ResponseEntity<>('dmi-response', HttpStatus.OK)
127 when: 'get resource data operational for cm-handle is called'
128 def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
133 then: 'DMI returns a json response'
134 response == 'dmi-response'
137 def 'Get resource data for pass-through running from DMI.'() {
138 given: 'cpsDataService returns valid data node'
139 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
140 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
141 and: 'DMI returns valid response and data'
142 mockDmiDataOperations.getResourceDataFromDmi('testCmHandle',
147 NO_TOPIC) >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
148 when: 'get resource data is called'
149 def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
154 then: 'get resource data returns expected response'
155 response == '{dmi-response}'
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 'Getting Yang Resources with an invalid #scenario.'() {
166 when: 'yang resources is called'
167 objectUnderTest.getYangResourcesModuleReferences('invalid cm handle with spaces')
168 then: 'a data validation exception is thrown'
169 thrown(DataValidationException)
170 and: 'CPS module services is not invoked'
171 0 * mockInventoryPersistence.getYangResourcesModuleReferences(*_)
174 def 'Get a cm handle.'() {
175 given: 'the system returns a yang modelled cm handle'
176 def dmiServiceName = 'some service name'
177 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED,
178 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.LOCKED_MODULE_SYNC_FAILED).details("lock details").build(),
179 lastUpdateTime: 'some-timestamp',
180 dataSyncEnabled: false,
181 dataStores: dataStores())
182 def dmiProperties = [new YangModelCmHandle.Property('Book', 'Romance Novel')]
183 def publicProperties = [new YangModelCmHandle.Property('Public Book', 'Public Romance Novel')]
184 def yangModelCmHandle = new YangModelCmHandle(id: 'some-cm-handle', dmiServiceName: dmiServiceName,
185 dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
186 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
187 when: 'getting cm handle details for a given cm handle id from ncmp service'
188 def result = objectUnderTest.getNcmpServiceCmHandle('some-cm-handle')
189 then: 'the result is a ncmpServiceCmHandle'
190 result.class == NcmpServiceCmHandle.class
191 and: 'the cm handle contains the cm handle id'
192 result.cmHandleId == 'some-cm-handle'
193 and: 'the cm handle contains the DMI Properties'
194 result.dmiProperties ==[ Book:'Romance Novel' ]
195 and: 'the cm handle contains the public Properties'
196 result.publicProperties == [ "Public Book":'Public Romance Novel' ]
197 and: 'the cm handle contains the cm handle composite state'
198 result.compositeState == compositeState
202 def 'Get a cm handle with an invalid id.'() {
203 when: 'getting cm handle details for a given cm handle id with an invalid name'
204 objectUnderTest.getNcmpServiceCmHandle('invalid cm handle with spaces')
205 then: 'an exception is thrown'
206 thrown(DataValidationException)
207 and: 'the yang model cm handle retriever is not invoked'
208 0 * mockInventoryPersistence.getYangModelCmHandle(*_)
211 def 'Get cm handle public properties'() {
212 given: 'a yang modelled cm handle'
213 def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
214 def publicProperties = [new YangModelCmHandle.Property('public prop', 'some public prop')]
215 def yangModelCmHandle = new YangModelCmHandle(id:'some-cm-handle', dmiServiceName: 'some service name', dmiProperties: dmiProperties, publicProperties: publicProperties)
216 and: 'the system returns this yang modelled cm handle'
217 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
218 when: 'getting cm handle public properties for a given cm handle id from ncmp service'
219 def result = objectUnderTest.getCmHandlePublicProperties('some-cm-handle')
220 then: 'the result returns the correct data'
221 result == [ 'public prop' : 'some public prop' ]
224 def 'Get cm handle public properties with an invalid id.'() {
225 when: 'getting cm handle public properties for a given cm handle id with an invalid name'
226 objectUnderTest.getCmHandlePublicProperties('invalid cm handle with spaces')
227 then: 'an exception is thrown'
228 thrown(DataValidationException)
229 and: 'the yang model cm handle retriever is not invoked'
230 0 * mockInventoryPersistence.getYangModelCmHandle(*_)
233 def 'Get cm handle composite state'() {
234 given: 'a yang modelled cm handle'
235 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED,
236 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.LOCKED_MODULE_SYNC_FAILED).details("lock details").build(),
237 lastUpdateTime: 'some-timestamp',
238 dataSyncEnabled: false,
239 dataStores: dataStores())
240 def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
241 def publicProperties = [new YangModelCmHandle.Property('public prop', 'some public prop')]
242 def yangModelCmHandle = new YangModelCmHandle(id:'some-cm-handle', dmiServiceName: 'some service name', dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
243 and: 'the system returns this yang modelled cm handle'
244 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
245 when: 'getting cm handle composite state for a given cm handle id from ncmp service'
246 def result = objectUnderTest.getCmHandleCompositeState('some-cm-handle')
247 then: 'the result returns the correct data'
248 result == compositeState
251 def 'Get cm handle composite state with an invalid id.'() {
252 when: 'getting cm handle composite state for a given cm handle id with an invalid name'
253 objectUnderTest.getCmHandleCompositeState('invalid cm handle with spaces')
254 then: 'an exception is thrown'
255 thrown(DataValidationException)
256 and: 'the yang model cm handle retriever is not invoked'
257 0 * mockInventoryPersistence.getYangModelCmHandle(_)
260 def 'Update resource data for pass-through running from dmi using POST #scenario DMI properties.'() {
261 given: 'cpsDataService returns valid datanode'
262 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
263 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
264 when: 'get resource data is called'
265 objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
266 'testResourceId', UPDATE,
267 '{some-json}', 'application/json')
268 then: 'DMI called with correct data'
269 1 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi('testCmHandle', 'testResourceId',
270 UPDATE, '{some-json}', 'application/json')
271 >> { new ResponseEntity<>(HttpStatus.OK) }
274 def 'Verify modules and create anchor params'() {
275 given: 'dmi plugin registration return created cm handles'
276 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'service1', dmiModelPlugin: 'service1',
277 dmiDataPlugin: 'service2')
278 dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
279 mockDmiPluginRegistration.getCreatedCmHandles() >> [ncmpServiceCmHandle]
280 when: 'parse and create cm handle in dmi registration then sync module'
281 objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(mockDmiPluginRegistration)
282 then: 'system persists the cm handle state'
283 1 * mockLcmEventsCmHandleStateHandler.updateCmHandleState(_, _) >> {
285 def result = (args[0] as YangModelCmHandle)
286 assert result.id == 'test-cm-handle-id'
287 assert CmHandleState.ADVISED == (args[1] as CmHandleState)
292 def 'Execute cm handle id search'() {
293 given: 'valid CmHandleQueryApiParameters input'
294 def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
295 def conditionApiProperties = new ConditionApiProperties()
296 conditionApiProperties.conditionName = 'hasAllModules'
297 conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
298 cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
299 and: 'query cm handle method return with a data node list'
300 mockCpsCmHandlerQueryService.queryCmHandleIds(
301 spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
302 >> ['cm-handle-id-1']
303 when: 'execute cm handle search is called'
304 def result = objectUnderTest.executeCmHandleIdSearch(cmHandleQueryApiParameters)
305 then: 'result is the same collection as returned by the CPS Data Service'
306 assert result == ['cm-handle-id-1'] as Set
309 def 'Getting module definitions.'() {
310 when: 'get module definitions method is called with a valid cm handle ID'
311 objectUnderTest.getModuleDefinitionsByCmHandleId('some-cm-handle')
312 then: 'CPS module services is invoked once'
313 1 * mockInventoryPersistence.getModuleDefinitionsByCmHandleId('some-cm-handle')
316 def 'Execute cm handle search'() {
317 given: 'valid CmHandleQueryApiParameters input'
318 def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
319 def conditionApiProperties = new ConditionApiProperties()
320 conditionApiProperties.conditionName = 'hasAllModules'
321 conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
322 cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
323 and: 'query cm handle method return with a data node list'
324 mockCpsCmHandlerQueryService.queryCmHandles(
325 spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
326 >> [new NcmpServiceCmHandle(cmHandleId: 'cm-handle-id-1')]
327 when: 'execute cm handle search is called'
328 def result = objectUnderTest.executeCmHandleSearch(cmHandleQueryApiParameters)
329 then: 'result is the same collection as returned by the CPS Data Service'
330 assert result.stream().map(d -> d.cmHandleId).collect(Collectors.toSet()) == ['cm-handle-id-1'] as Set
333 def 'Set Cm Handle Data Sync Enabled Flag where data sync flag is #scenario'() {
334 given: 'an existing cm handle composite state'
335 def compositeState = new CompositeState(cmHandleState: CmHandleState.READY, dataSyncEnabled: initialDataSyncEnabledFlag,
336 dataStores: CompositeState.DataStores.builder()
337 .operationalDataStore(CompositeState.Operational.builder()
338 .dataStoreSyncState(initialDataSyncState)
340 and: 'get cm handle state returns the composite state for the given cm handle id'
341 mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
342 when: 'set data sync enabled is called with the data sync enabled flag set to #dataSyncEnabledFlag'
343 objectUnderTest.setDataSyncEnabled('some-cm-handle-id', dataSyncEnabledFlag)
344 then: 'the data sync enabled flag is set to #dataSyncEnabled'
345 compositeState.dataSyncEnabled == dataSyncEnabledFlag
346 and: 'the data store sync state is set to #expectedDataStoreSyncState'
347 compositeState.dataStores.operationalDataStore.dataStoreSyncState == expectedDataStoreSyncState
348 and: 'the cps data service to delete data nodes is invoked the expected number of times'
349 deleteDataNodeExpectedNumberOfInvocation * mockCpsDataService.deleteDataNode('NFP-Operational', 'some-cm-handle-id', '/netconf-state', _)
350 and: 'the inventory persistence service to update node leaves is called with the correct values'
351 saveCmHandleStateExpectedNumberOfInvocations * mockInventoryPersistence.saveCmHandleState('some-cm-handle-id', compositeState)
352 where: 'the following data sync enabled flag is used'
353 scenario | dataSyncEnabledFlag | initialDataSyncEnabledFlag | initialDataSyncState || expectedDataStoreSyncState | deleteDataNodeExpectedNumberOfInvocation | saveCmHandleStateExpectedNumberOfInvocations
354 'enabled' | true | false | DataStoreSyncState.NONE_REQUESTED || DataStoreSyncState.UNSYNCHRONIZED | 0 | 1
355 'disabled' | false | true | DataStoreSyncState.UNSYNCHRONIZED || DataStoreSyncState.NONE_REQUESTED | 0 | 1
356 'disabled where sync-state is currently SYNCHRONIZED' | false | true | DataStoreSyncState.SYNCHRONIZED || DataStoreSyncState.NONE_REQUESTED | 1 | 1
357 'is set to existing flag state' | true | true | DataStoreSyncState.UNSYNCHRONIZED || DataStoreSyncState.UNSYNCHRONIZED | 0 | 0
360 def 'Set cm Handle Data Sync Enabled flag with following cm handle not in ready state exception' () {
361 given: 'a cm handle composite state'
362 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED, dataSyncEnabled: false)
363 and: 'get cm handle state returns the composite state for the given cm handle id'
364 mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
365 when: 'set data sync enabled is called with the data sync enabled flag set to true'
366 objectUnderTest.setDataSyncEnabled('some-cm-handle-id', true)
367 then: 'the expected exception is thrown'
369 and: 'the inventory persistence service to update node leaves is not invoked'
370 0 * mockInventoryPersistence.saveCmHandleState(_, _)
374 CompositeState.DataStores.builder()
375 .operationalDataStore(CompositeState.Operational.builder()
376 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
377 .lastSyncTime('some-timestamp').build()).build()