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.CmHandleQueries
29 import org.onap.cps.ncmp.api.inventory.CmHandleState
30 import org.onap.cps.ncmp.api.inventory.CompositeState
31 import org.onap.cps.ncmp.api.inventory.InventoryPersistence
32 import org.onap.cps.ncmp.api.inventory.LockReasonCategory
33 import org.onap.cps.ncmp.api.inventory.DataStoreSyncState
34 import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters
35 import org.onap.cps.ncmp.api.models.ConditionApiProperties
36 import org.onap.cps.ncmp.api.models.DmiPluginRegistration
37 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
38 import org.onap.cps.spi.exceptions.CpsException
39 import org.onap.cps.spi.exceptions.DataValidationException
40 import org.onap.cps.spi.model.CmHandleQueryServiceParameters
41 import spock.lang.Shared
43 import java.util.stream.Collectors
45 import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL
46 import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING
47 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.CREATE
48 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.UPDATE
50 import org.onap.cps.utils.JsonObjectMapper
51 import com.fasterxml.jackson.databind.ObjectMapper
52 import org.onap.cps.api.CpsDataService
53 import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
54 import org.onap.cps.spi.FetchDescendantsOption
55 import org.onap.cps.spi.model.DataNode
56 import org.springframework.http.HttpStatus
57 import org.springframework.http.ResponseEntity
58 import spock.lang.Specification
60 class NetworkCmProxyDataServiceImplSpec extends Specification {
62 def mockCpsDataService = Mock(CpsDataService)
63 def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
64 def mockDmiDataOperations = Mock(DmiDataOperations)
65 def nullNetworkCmProxyDataServicePropertyHandler = null
66 def mockInventoryPersistence = Mock(InventoryPersistence)
67 def mockCmHandleQueries = Mock(CmHandleQueries)
68 def mockDmiPluginRegistration = Mock(DmiPluginRegistration)
69 def mockCpsCmHandlerQueryService = Mock(NetworkCmProxyCmHandlerQueryService)
70 def mockLcmEventsCmHandleStateHandler = Mock(LcmEventsCmHandleStateHandler)
73 def NO_REQUEST_ID = null
75 def OPTIONS_PARAM = '(a=1,b=2)'
77 def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'test-cm-handle-id')
79 def objectUnderTest = new NetworkCmProxyDataServiceImpl(
80 spiedJsonObjectMapper,
81 mockDmiDataOperations,
82 nullNetworkCmProxyDataServicePropertyHandler,
83 mockInventoryPersistence,
85 mockCpsCmHandlerQueryService,
86 mockLcmEventsCmHandleStateHandler,
89 def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
91 def dataNode = new DataNode(leaves: ['id': 'some-cm-handle', 'dmi-service-name': 'testDmiService'])
93 def 'Write resource data for pass-through running from DMI using POST.'() {
94 given: 'cpsDataService returns valid datanode'
95 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
96 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
97 when: 'write resource data is called'
98 objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
99 'testResourceId', CREATE,
100 '{some-json}', 'application/json')
101 then: 'DMI called with correct data'
102 1 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi('testCmHandle', 'testResourceId',
103 CREATE, '{some-json}', 'application/json')
104 >> { new ResponseEntity<>(HttpStatus.CREATED) }
107 def 'Write resource data for pass-through running from DMI using an invalid id.'() {
108 when: 'write resource data is called'
109 objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('invalid cm handle name',
110 'testResourceId', CREATE,
111 '{some-json}', 'application/json')
112 then: 'exception is thrown'
113 thrown(DataValidationException.class)
114 and: 'DMI is not invoked'
115 0 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi(_, _, _, _, _)
118 def 'Get resource data for pass-through operational from DMI.'() {
119 given: 'get data node is called'
120 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
121 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
122 and: 'get resource data from DMI is called'
123 mockDmiDataOperations.getResourceDataFromDmi(
127 PASSTHROUGH_OPERATIONAL,
129 NO_TOPIC) >> new ResponseEntity<>('dmi-response', HttpStatus.OK)
130 when: 'get resource data operational for cm-handle is called'
131 def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
136 then: 'DMI returns a json response'
137 response == 'dmi-response'
140 def 'Get resource data for pass-through running from DMI.'() {
141 given: 'cpsDataService returns valid data node'
142 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
143 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
144 and: 'DMI returns valid response and data'
145 mockDmiDataOperations.getResourceDataFromDmi('testCmHandle',
150 NO_TOPIC) >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
151 when: 'get resource data is called'
152 def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
157 then: 'get resource data returns expected response'
158 response == '{dmi-response}'
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 'Getting Yang Resources with an invalid #scenario.'() {
169 when: 'yang resources is called'
170 objectUnderTest.getYangResourcesModuleReferences('invalid cm handle with spaces')
171 then: 'a data validation exception is thrown'
172 thrown(DataValidationException)
173 and: 'CPS module services is not invoked'
174 0 * mockInventoryPersistence.getYangResourcesModuleReferences(*_)
177 def 'Get a cm handle.'() {
178 given: 'the system returns a yang modelled cm handle'
179 def dmiServiceName = 'some service name'
180 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED,
181 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.LOCKED_MODULE_SYNC_FAILED).details("lock details").build(),
182 lastUpdateTime: 'some-timestamp',
183 dataSyncEnabled: false,
184 dataStores: dataStores())
185 def dmiProperties = [new YangModelCmHandle.Property('Book', 'Romance Novel')]
186 def publicProperties = [new YangModelCmHandle.Property('Public Book', 'Public Romance Novel')]
187 def yangModelCmHandle = new YangModelCmHandle(id: 'some-cm-handle', dmiServiceName: dmiServiceName,
188 dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
189 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
190 when: 'getting cm handle details for a given cm handle id from ncmp service'
191 def result = objectUnderTest.getNcmpServiceCmHandle('some-cm-handle')
192 then: 'the result is a ncmpServiceCmHandle'
193 result.class == NcmpServiceCmHandle.class
194 and: 'the cm handle contains the cm handle id'
195 result.cmHandleId == 'some-cm-handle'
196 and: 'the cm handle contains the DMI Properties'
197 result.dmiProperties ==[ Book:'Romance Novel' ]
198 and: 'the cm handle contains the public Properties'
199 result.publicProperties == [ "Public Book":'Public Romance Novel' ]
200 and: 'the cm handle contains the cm handle composite state'
201 result.compositeState == compositeState
205 def 'Get a cm handle with an invalid id.'() {
206 when: 'getting cm handle details for a given cm handle id with an invalid name'
207 objectUnderTest.getNcmpServiceCmHandle('invalid cm handle with spaces')
208 then: 'an exception is thrown'
209 thrown(DataValidationException)
210 and: 'the yang model cm handle retriever is not invoked'
211 0 * mockInventoryPersistence.getYangModelCmHandle(*_)
214 def 'Get cm handle public properties'() {
215 given: 'a yang modelled cm handle'
216 def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
217 def publicProperties = [new YangModelCmHandle.Property('public prop', 'some public prop')]
218 def yangModelCmHandle = new YangModelCmHandle(id:'some-cm-handle', dmiServiceName: 'some service name', dmiProperties: dmiProperties, publicProperties: publicProperties)
219 and: 'the system returns this yang modelled cm handle'
220 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
221 when: 'getting cm handle public properties for a given cm handle id from ncmp service'
222 def result = objectUnderTest.getCmHandlePublicProperties('some-cm-handle')
223 then: 'the result returns the correct data'
224 result == [ 'public prop' : 'some public prop' ]
227 def 'Get cm handle public properties with an invalid id.'() {
228 when: 'getting cm handle public properties for a given cm handle id with an invalid name'
229 objectUnderTest.getCmHandlePublicProperties('invalid cm handle with spaces')
230 then: 'an exception is thrown'
231 thrown(DataValidationException)
232 and: 'the yang model cm handle retriever is not invoked'
233 0 * mockInventoryPersistence.getYangModelCmHandle(*_)
236 def 'Get cm handle composite state'() {
237 given: 'a yang modelled cm handle'
238 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED,
239 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.LOCKED_MODULE_SYNC_FAILED).details("lock details").build(),
240 lastUpdateTime: 'some-timestamp',
241 dataSyncEnabled: false,
242 dataStores: dataStores())
243 def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
244 def publicProperties = [new YangModelCmHandle.Property('public prop', 'some public prop')]
245 def yangModelCmHandle = new YangModelCmHandle(id:'some-cm-handle', dmiServiceName: 'some service name', dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
246 and: 'the system returns this yang modelled cm handle'
247 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
248 when: 'getting cm handle composite state for a given cm handle id from ncmp service'
249 def result = objectUnderTest.getCmHandleCompositeState('some-cm-handle')
250 then: 'the result returns the correct data'
251 result == compositeState
254 def 'Get cm handle composite state with an invalid id.'() {
255 when: 'getting cm handle composite state for a given cm handle id with an invalid name'
256 objectUnderTest.getCmHandleCompositeState('invalid cm handle with spaces')
257 then: 'an exception is thrown'
258 thrown(DataValidationException)
259 and: 'the yang model cm handle retriever is not invoked'
260 0 * mockInventoryPersistence.getYangModelCmHandle(_)
263 def 'Update resource data for pass-through running from dmi using POST #scenario DMI properties.'() {
264 given: 'cpsDataService returns valid datanode'
265 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
266 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
267 when: 'get resource data is called'
268 objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
269 'testResourceId', UPDATE,
270 '{some-json}', 'application/json')
271 then: 'DMI called with correct data'
272 1 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi('testCmHandle', 'testResourceId',
273 UPDATE, '{some-json}', 'application/json')
274 >> { new ResponseEntity<>(HttpStatus.OK) }
277 def 'Verify modules and create anchor params'() {
278 given: 'dmi plugin registration return created cm handles'
279 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'service1', dmiModelPlugin: 'service1',
280 dmiDataPlugin: 'service2')
281 dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
282 mockDmiPluginRegistration.getCreatedCmHandles() >> [ncmpServiceCmHandle]
283 when: 'parse and create cm handle in dmi registration then sync module'
284 objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(mockDmiPluginRegistration)
285 then: 'system persists the cm handle state'
286 1 * mockLcmEventsCmHandleStateHandler.updateCmHandleState(_, _) >> {
288 def result = (args[0] as YangModelCmHandle)
289 assert result.id == 'test-cm-handle-id'
290 assert CmHandleState.ADVISED == (args[1] as CmHandleState)
295 def 'Execute cm handle id 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.queryCmHandleIds(
304 spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
305 >> ['cm-handle-id-1']
306 when: 'execute cm handle search is called'
307 def result = objectUnderTest.executeCmHandleIdSearch(cmHandleQueryApiParameters)
308 then: 'result is the same collection as returned by the CPS Data Service'
309 assert result == ['cm-handle-id-1'] as Set
312 def 'Getting module definitions.'() {
313 when: 'get module definitions method is called with a valid cm handle ID'
314 objectUnderTest.getModuleDefinitionsByCmHandleId('some-cm-handle')
315 then: 'CPS module services is invoked once'
316 1 * mockInventoryPersistence.getModuleDefinitionsByCmHandleId('some-cm-handle')
319 def 'Execute cm handle search'() {
320 given: 'valid CmHandleQueryApiParameters input'
321 def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
322 def conditionApiProperties = new ConditionApiProperties()
323 conditionApiProperties.conditionName = 'hasAllModules'
324 conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
325 cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
326 and: 'query cm handle method return with a data node list'
327 mockCpsCmHandlerQueryService.queryCmHandles(
328 spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
329 >> [new NcmpServiceCmHandle(cmHandleId: 'cm-handle-id-1')]
330 when: 'execute cm handle search is called'
331 def result = objectUnderTest.executeCmHandleSearch(cmHandleQueryApiParameters)
332 then: 'result is the same collection as returned by the CPS Data Service'
333 assert result.stream().map(d -> d.cmHandleId).collect(Collectors.toSet()) == ['cm-handle-id-1'] as Set
336 def 'Set Cm Handle Data Sync Enabled Flag where data sync flag is #scenario'() {
337 given: 'an existing cm handle composite state'
338 def compositeState = new CompositeState(cmHandleState: CmHandleState.READY, dataSyncEnabled: initialDataSyncEnabledFlag,
339 dataStores: CompositeState.DataStores.builder()
340 .operationalDataStore(CompositeState.Operational.builder()
341 .dataStoreSyncState(initialDataSyncState)
343 and: 'get cm handle state returns the composite state for the given cm handle id'
344 mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
345 when: 'set data sync enabled is called with the data sync enabled flag set to #dataSyncEnabledFlag'
346 objectUnderTest.setDataSyncEnabled('some-cm-handle-id', dataSyncEnabledFlag)
347 then: 'the data sync enabled flag is set to #dataSyncEnabled'
348 compositeState.dataSyncEnabled == dataSyncEnabledFlag
349 and: 'the data store sync state is set to #expectedDataStoreSyncState'
350 compositeState.dataStores.operationalDataStore.dataStoreSyncState == expectedDataStoreSyncState
351 and: 'the cps data service to delete data nodes is invoked the expected number of times'
352 deleteDataNodeExpectedNumberOfInvocation * mockCpsDataService.deleteDataNode('NFP-Operational', 'some-cm-handle-id', '/netconf-state', _)
353 and: 'the inventory persistence service to update node leaves is called with the correct values'
354 saveCmHandleStateExpectedNumberOfInvocations * mockInventoryPersistence.saveCmHandleState('some-cm-handle-id', compositeState)
355 where: 'the following data sync enabled flag is used'
356 scenario | dataSyncEnabledFlag | initialDataSyncEnabledFlag | initialDataSyncState || expectedDataStoreSyncState | deleteDataNodeExpectedNumberOfInvocation | saveCmHandleStateExpectedNumberOfInvocations
357 'enabled' | true | false | DataStoreSyncState.NONE_REQUESTED || DataStoreSyncState.UNSYNCHRONIZED | 0 | 1
358 'disabled' | false | true | DataStoreSyncState.UNSYNCHRONIZED || DataStoreSyncState.NONE_REQUESTED | 0 | 1
359 'disabled where sync-state is currently SYNCHRONIZED' | false | true | DataStoreSyncState.SYNCHRONIZED || DataStoreSyncState.NONE_REQUESTED | 1 | 1
360 'is set to existing flag state' | true | true | DataStoreSyncState.UNSYNCHRONIZED || DataStoreSyncState.UNSYNCHRONIZED | 0 | 0
363 def 'Set cm Handle Data Sync Enabled flag with following cm handle not in ready state exception' () {
364 given: 'a cm handle composite state'
365 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED, dataSyncEnabled: false)
366 and: 'get cm handle state returns the composite state for the given cm handle id'
367 mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
368 when: 'set data sync enabled is called with the data sync enabled flag set to true'
369 objectUnderTest.setDataSyncEnabled('some-cm-handle-id', true)
370 then: 'the expected exception is thrown'
372 and: 'the inventory persistence service to update node leaves is not invoked'
373 0 * mockInventoryPersistence.saveCmHandleState(_, _)
376 def 'Get all cm handle IDs by DMI plugin identifier.' () {
377 given: 'cm handle queries service returns cm handles'
378 1 * mockCmHandleQueries.getCmHandlesByDmiPluginIdentifier('some-dmi-plugin-identifier')
379 >> [new NcmpServiceCmHandle(cmHandleId: 'cm-handle-1'),
380 new NcmpServiceCmHandle(cmHandleId: 'cm-handle-2')]
381 when: 'cm handle Ids are requested with dmi plugin identifier'
382 def result = objectUnderTest.getAllCmHandleIdsByDmiPluginIdentifier('some-dmi-plugin-identifier')
383 then: 'the result size is correct'
384 assert result.size() == 2
385 and: 'the result returns the correct details'
386 assert result.containsAll('cm-handle-1','cm-handle-2')
390 CompositeState.DataStores.builder()
391 .operationalDataStore(CompositeState.Operational.builder()
392 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
393 .lastSyncTime('some-timestamp').build()).build()