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 com.hazelcast.map.IMap
26 import org.onap.cps.ncmp.api.NetworkCmProxyCmHandlerQueryService
27 import org.onap.cps.ncmp.api.impl.event.lcm.LcmEventsCmHandleStateHandler
28 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
29 import org.onap.cps.ncmp.api.inventory.CmHandleQueries
30 import org.onap.cps.ncmp.api.inventory.CmHandleState
31 import org.onap.cps.ncmp.api.inventory.CompositeState
32 import org.onap.cps.ncmp.api.inventory.InventoryPersistence
33 import org.onap.cps.ncmp.api.inventory.LockReasonCategory
34 import org.onap.cps.ncmp.api.inventory.DataStoreSyncState
35 import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters
36 import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse
37 import org.onap.cps.ncmp.api.models.ConditionApiProperties
38 import org.onap.cps.ncmp.api.models.DmiPluginRegistration
39 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
40 import org.onap.cps.spi.exceptions.CpsException
41 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
42 import org.onap.cps.spi.exceptions.DataValidationException
43 import org.onap.cps.spi.model.CmHandleQueryServiceParameters
44 import spock.lang.Shared
45 import java.util.stream.Collectors
46 import org.onap.cps.utils.JsonObjectMapper
47 import com.fasterxml.jackson.databind.ObjectMapper
48 import org.onap.cps.api.CpsDataService
49 import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
50 import org.onap.cps.spi.FetchDescendantsOption
51 import org.onap.cps.spi.model.DataNode
52 import org.springframework.http.HttpStatus
53 import org.springframework.http.ResponseEntity
54 import spock.lang.Specification
56 import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL
57 import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING
58 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.CREATE
59 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.UPDATE
60 import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_DOES_NOT_EXIST
61 import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_INVALID_ID
62 import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.UNKNOWN_ERROR
65 class NetworkCmProxyDataServiceImplSpec extends Specification {
67 def mockCpsDataService = Mock(CpsDataService)
68 def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
69 def mockDmiDataOperations = Mock(DmiDataOperations)
70 def nullNetworkCmProxyDataServicePropertyHandler = null
71 def mockInventoryPersistence = Mock(InventoryPersistence)
72 def mockCmHandleQueries = Mock(CmHandleQueries)
73 def mockDmiPluginRegistration = Mock(DmiPluginRegistration)
74 def mockCpsCmHandlerQueryService = Mock(NetworkCmProxyCmHandlerQueryService)
75 def mockLcmEventsCmHandleStateHandler = Mock(LcmEventsCmHandleStateHandler)
76 def stubModuleSyncStartedOnCmHandles = Stub(IMap<String, Object>)
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)
96 def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
98 def dataNode = new DataNode(leaves: ['id': 'some-cm-handle', 'dmi-service-name': 'testDmiService'])
100 def 'Write resource data for pass-through running from DMI using POST.'() {
101 given: 'cpsDataService returns valid datanode'
102 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
103 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> 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: 'get data node is called'
116 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
117 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
118 and: 'get resource data from DMI is called'
119 mockDmiDataOperations.getResourceDataFromDmi(
123 PASSTHROUGH_OPERATIONAL,
125 NO_TOPIC) >> new ResponseEntity<>('dmi-response', HttpStatus.OK)
126 when: 'get resource data operational for cm-handle is called'
127 def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
132 then: 'DMI returns a json response'
133 response == 'dmi-response'
136 def 'Get resource data for pass-through running from DMI.'() {
137 given: 'cpsDataService returns valid data node'
138 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
139 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
140 and: 'DMI returns valid response and data'
141 mockDmiDataOperations.getResourceDataFromDmi('testCmHandle',
146 NO_TOPIC) >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
147 when: 'get resource data is called'
148 def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
153 then: 'get resource data returns expected response'
154 response == '{dmi-response}'
157 def 'Getting Yang Resources.'() {
158 when: 'yang resources is called'
159 objectUnderTest.getYangResourcesModuleReferences('some-cm-handle')
160 then: 'CPS module services is invoked for the correct dataspace and cm handle'
161 1 * mockInventoryPersistence.getYangResourcesModuleReferences('some-cm-handle')
164 def 'Get a cm handle.'() {
165 given: 'the system returns a yang modelled cm handle'
166 def dmiServiceName = 'some service name'
167 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED,
168 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.LOCKED_MODULE_SYNC_FAILED).details("lock details").build(),
169 lastUpdateTime: 'some-timestamp',
170 dataSyncEnabled: false,
171 dataStores: dataStores())
172 def dmiProperties = [new YangModelCmHandle.Property('Book', 'Romance Novel')]
173 def publicProperties = [new YangModelCmHandle.Property('Public Book', 'Public Romance Novel')]
174 def yangModelCmHandle = new YangModelCmHandle(id: 'some-cm-handle', dmiServiceName: dmiServiceName,
175 dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
176 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
177 when: 'getting cm handle details for a given cm handle id from ncmp service'
178 def result = objectUnderTest.getNcmpServiceCmHandle('some-cm-handle')
179 then: 'the result is a ncmpServiceCmHandle'
180 result.class == NcmpServiceCmHandle.class
181 and: 'the cm handle contains the cm handle id'
182 result.cmHandleId == 'some-cm-handle'
183 and: 'the cm handle contains the DMI Properties'
184 result.dmiProperties ==[ Book:'Romance Novel' ]
185 and: 'the cm handle contains the public Properties'
186 result.publicProperties == [ "Public Book":'Public Romance Novel' ]
187 and: 'the cm handle contains the cm handle composite state'
188 result.compositeState == compositeState
192 def 'Get cm handle public properties'() {
193 given: 'a yang modelled cm handle'
194 def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
195 def publicProperties = [new YangModelCmHandle.Property('public prop', 'some public prop')]
196 def yangModelCmHandle = new YangModelCmHandle(id:'some-cm-handle', dmiServiceName: 'some service name', dmiProperties: dmiProperties, publicProperties: publicProperties)
197 and: 'the system returns this yang modelled cm handle'
198 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
199 when: 'getting cm handle public properties for a given cm handle id from ncmp service'
200 def result = objectUnderTest.getCmHandlePublicProperties('some-cm-handle')
201 then: 'the result returns the correct data'
202 result == [ 'public prop' : 'some public prop' ]
205 def 'Get cm handle composite state'() {
206 given: 'a yang modelled cm handle'
207 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED,
208 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.LOCKED_MODULE_SYNC_FAILED).details("lock details").build(),
209 lastUpdateTime: 'some-timestamp',
210 dataSyncEnabled: false,
211 dataStores: dataStores())
212 def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
213 def publicProperties = [new YangModelCmHandle.Property('public prop', 'some public prop')]
214 def yangModelCmHandle = new YangModelCmHandle(id:'some-cm-handle', dmiServiceName: 'some service name', dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
215 and: 'the system returns this yang modelled cm handle'
216 1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
217 when: 'getting cm handle composite state for a given cm handle id from ncmp service'
218 def result = objectUnderTest.getCmHandleCompositeState('some-cm-handle')
219 then: 'the result returns the correct data'
220 result == compositeState
223 def 'Update resource data for pass-through running from dmi using POST #scenario DMI properties.'() {
224 given: 'cpsDataService returns valid datanode'
225 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
226 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
227 when: 'get resource data is called'
228 objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
229 'testResourceId', UPDATE,
230 '{some-json}', 'application/json')
231 then: 'DMI called with correct data'
232 1 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi('testCmHandle', 'testResourceId',
233 UPDATE, '{some-json}', 'application/json')
234 >> { new ResponseEntity<>(HttpStatus.OK) }
237 def 'Verify modules and create anchor params'() {
238 given: 'dmi plugin registration return created cm handles'
239 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'service1', dmiModelPlugin: 'service1',
240 dmiDataPlugin: 'service2')
241 dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
242 mockDmiPluginRegistration.getCreatedCmHandles() >> [ncmpServiceCmHandle]
243 when: 'parse and create cm handle in dmi registration then sync module'
244 objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(mockDmiPluginRegistration)
245 then: 'system persists the cm handle state'
246 1 * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(_) >> {
249 def cmHandleStatePerCmHandle = (args[0] as Map)
250 cmHandleStatePerCmHandle.each {
251 assert (it.key.id == 'test-cm-handle-id'
252 && it.value == CmHandleState.ADVISED)
258 def 'Execute cm handle id search'() {
259 given: 'valid CmHandleQueryApiParameters input'
260 def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
261 def conditionApiProperties = new ConditionApiProperties()
262 conditionApiProperties.conditionName = 'hasAllModules'
263 conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
264 cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
265 and: 'query cm handle method return with a data node list'
266 mockCpsCmHandlerQueryService.queryCmHandleIds(
267 spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
268 >> ['cm-handle-id-1']
269 when: 'execute cm handle search is called'
270 def result = objectUnderTest.executeCmHandleIdSearch(cmHandleQueryApiParameters)
271 then: 'result is the same collection as returned by the CPS Data Service'
272 assert result == ['cm-handle-id-1'] as Set
275 def 'Getting module definitions.'() {
276 when: 'get module definitions method is called with a valid cm handle ID'
277 objectUnderTest.getModuleDefinitionsByCmHandleId('some-cm-handle')
278 then: 'CPS module services is invoked once'
279 1 * mockInventoryPersistence.getModuleDefinitionsByCmHandleId('some-cm-handle')
282 def 'Execute cm handle search'() {
283 given: 'valid CmHandleQueryApiParameters input'
284 def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
285 def conditionApiProperties = new ConditionApiProperties()
286 conditionApiProperties.conditionName = 'hasAllModules'
287 conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
288 cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
289 and: 'query cm handle method return with a data node list'
290 mockCpsCmHandlerQueryService.queryCmHandles(
291 spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
292 >> [new NcmpServiceCmHandle(cmHandleId: 'cm-handle-id-1')]
293 when: 'execute cm handle search is called'
294 def result = objectUnderTest.executeCmHandleSearch(cmHandleQueryApiParameters)
295 then: 'result is the same collection as returned by the CPS Data Service'
296 assert result.stream().map(d -> d.cmHandleId).collect(Collectors.toSet()) == ['cm-handle-id-1'] as Set
299 def 'Set Cm Handle Data Sync Enabled Flag where data sync flag is #scenario'() {
300 given: 'an existing cm handle composite state'
301 def compositeState = new CompositeState(cmHandleState: CmHandleState.READY, dataSyncEnabled: initialDataSyncEnabledFlag,
302 dataStores: CompositeState.DataStores.builder()
303 .operationalDataStore(CompositeState.Operational.builder()
304 .dataStoreSyncState(initialDataSyncState)
306 and: 'get cm handle state returns the composite state for the given cm handle id'
307 mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
308 when: 'set data sync enabled is called with the data sync enabled flag set to #dataSyncEnabledFlag'
309 objectUnderTest.setDataSyncEnabled('some-cm-handle-id', dataSyncEnabledFlag)
310 then: 'the data sync enabled flag is set to #dataSyncEnabled'
311 compositeState.dataSyncEnabled == dataSyncEnabledFlag
312 and: 'the data store sync state is set to #expectedDataStoreSyncState'
313 compositeState.dataStores.operationalDataStore.dataStoreSyncState == expectedDataStoreSyncState
314 and: 'the cps data service to delete data nodes is invoked the expected number of times'
315 deleteDataNodeExpectedNumberOfInvocation * mockCpsDataService.deleteDataNode('NFP-Operational', 'some-cm-handle-id', '/netconf-state', _)
316 and: 'the inventory persistence service to update node leaves is called with the correct values'
317 saveCmHandleStateExpectedNumberOfInvocations * mockInventoryPersistence.saveCmHandleState('some-cm-handle-id', compositeState)
318 where: 'the following data sync enabled flag is used'
319 scenario | dataSyncEnabledFlag | initialDataSyncEnabledFlag | initialDataSyncState || expectedDataStoreSyncState | deleteDataNodeExpectedNumberOfInvocation | saveCmHandleStateExpectedNumberOfInvocations
320 'enabled' | true | false | DataStoreSyncState.NONE_REQUESTED || DataStoreSyncState.UNSYNCHRONIZED | 0 | 1
321 'disabled' | false | true | DataStoreSyncState.UNSYNCHRONIZED || DataStoreSyncState.NONE_REQUESTED | 0 | 1
322 'disabled where sync-state is currently SYNCHRONIZED' | false | true | DataStoreSyncState.SYNCHRONIZED || DataStoreSyncState.NONE_REQUESTED | 1 | 1
323 'is set to existing flag state' | true | true | DataStoreSyncState.UNSYNCHRONIZED || DataStoreSyncState.UNSYNCHRONIZED | 0 | 0
326 def 'Set cm Handle Data Sync Enabled flag with following cm handle not in ready state exception' () {
327 given: 'a cm handle composite state'
328 def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED, dataSyncEnabled: false)
329 and: 'get cm handle state returns the composite state for the given cm handle id'
330 mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
331 when: 'set data sync enabled is called with the data sync enabled flag set to true'
332 objectUnderTest.setDataSyncEnabled('some-cm-handle-id', true)
333 then: 'the expected exception is thrown'
335 and: 'the inventory persistence service to update node leaves is not invoked'
336 0 * mockInventoryPersistence.saveCmHandleState(_, _)
339 def 'Get all cm handle IDs by DMI plugin identifier.' () {
340 given: 'cm handle queries service returns cm handles'
341 1 * mockCmHandleQueries.getCmHandlesByDmiPluginIdentifier('some-dmi-plugin-identifier')
342 >> [new NcmpServiceCmHandle(cmHandleId: 'cm-handle-1'),
343 new NcmpServiceCmHandle(cmHandleId: 'cm-handle-2')]
344 when: 'cm handle Ids are requested with dmi plugin identifier'
345 def result = objectUnderTest.getAllCmHandleIdsByDmiPluginIdentifier('some-dmi-plugin-identifier')
346 then: 'the result size is correct'
347 assert result.size() == 2
348 and: 'the result returns the correct details'
349 assert result.containsAll('cm-handle-1','cm-handle-2')
353 CompositeState.DataStores.builder()
354 .operationalDataStore(CompositeState.Operational.builder()
355 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
356 .lastSyncTime('some-timestamp').build()).build()