NCMP: Update existing Batch endpoint (Moving url param into rest body)
[cps.git] / cps-ncmp-service / src / test / groovy / org / onap / cps / ncmp / api / impl / NetworkCmProxyDataServiceImplSpec.groovy
1 /*
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
11  *
12  *        http://www.apache.org/licenses/LICENSE-2.0
13  *
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.
19  *
20  *  SPDX-License-Identifier: Apache-2.0
21  *  ============LICENSE_END=========================================================
22  */
23
24 package org.onap.cps.ncmp.api.impl
25
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.BatchOperationDefinition
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.ResourceDataBatchRequest
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
56
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
61
62 class NetworkCmProxyDataServiceImplSpec extends Specification {
63
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>)
74
75     def NO_TOPIC = null
76     def NO_REQUEST_ID = null
77     @Shared
78     def OPTIONS_PARAM = '(a=1,b=2)'
79     @Shared
80     def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'test-cm-handle-id')
81
82     def objectUnderTest = new NetworkCmProxyDataServiceImpl(
83             spiedJsonObjectMapper,
84             mockDmiDataOperations,
85             nullNetworkCmProxyDataServicePropertyHandler,
86             mockInventoryPersistence,
87             mockCmHandleQueries,
88             mockCpsCmHandlerQueryService,
89             mockLcmEventsCmHandleStateHandler,
90             mockCpsDataService,
91             stubModuleSyncStartedOnCmHandles)
92
93     def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
94
95     def dataNode = [new DataNode(leaves: ['id': 'some-cm-handle', 'dmi-service-name': 'testDmiService'])]
96
97     def 'Write resource data for pass-through running from DMI using POST.'() {
98         given: 'cpsDataService returns valid datanode'
99             mockDataNode()
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) }
108     }
109
110     def 'Get resource data for pass-through operational from DMI.'() {
111         given: 'get data node is called'
112             mockDataNode()
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'
122     }
123
124     def 'Get resource data for pass-through running from DMI.'() {
125         given: 'cpsDataService returns valid data node'
126             mockDataNode()
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}'
136     }
137
138     def 'Get batch resource data for #datastoreName from DMI.'() {
139         given: 'cpsDataService returns valid data node'
140             def resourceDataBatchRequest = getResourceDataBatchRequest(datastoreName)
141         when: 'get batch resource data is called'
142             objectUnderTest.requestResourceDataForCmHandleBatch('some topic', resourceDataBatchRequest, 'requestId')
143         then: 'get batch resource data returns expected response'
144             1 * mockDmiDataOperations.requestResourceDataFromDmi('some topic', resourceDataBatchRequest, 'requestId')
145         where: 'the following data stores are used'
146             datastoreName << [PASSTHROUGH_RUNNING.datastoreName, PASSTHROUGH_OPERATIONAL.datastoreName]
147     }
148
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')
154     }
155
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
181
182     }
183
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' ]
195     }
196
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')
212     }
213
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
230     }
231
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) }
244     }
245
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(_) >> {
256                 args ->
257                     {
258                         def cmHandleStatePerCmHandle = (args[0] as Map)
259                         cmHandleStatePerCmHandle.each {
260                             assert (it.key.id == 'test-cm-handle-id'
261                                     && it.value == CmHandleState.ADVISED)
262                         }
263                     }
264             }
265     }
266
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']
282     }
283
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')
289     }
290
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
306     }
307
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)
314                         .build()).build())
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
333     }
334
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'
343             thrown(CpsException)
344         and: 'the inventory persistence service to update node leaves is not invoked'
345             0 * mockInventoryPersistence.saveCmHandleState(_, _)
346     }
347
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')
357     }
358
359     def dataStores() {
360         CompositeState.DataStores.builder()
361             .operationalDataStore(CompositeState.Operational.builder()
362                 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
363                 .lastSyncTime('some-timestamp').build()).build()
364     }
365
366     def mockDataNode() {
367         mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
368                 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
369     }
370
371     def getResourceDataBatchRequest(datastore) {
372         def resourceDataBatchRequest = new ResourceDataBatchRequest()
373         def batchOperationDefinitions = new ArrayList()
374         batchOperationDefinitions.add(getBatchOperationDefinition(datastore))
375         resourceDataBatchRequest.setBatchOperationDefinitions(batchOperationDefinitions)
376     }
377
378     def getBatchOperationDefinition(datastore) {
379         def batchOperationDefinition = new BatchOperationDefinition()
380         batchOperationDefinition.setOperation("read")
381         batchOperationDefinition.setOperationId("operational-12")
382         batchOperationDefinition.setDatastore(datastore)
383         def targetIds = new ArrayList()
384         targetIds.add("some-cm-handle")
385         batchOperationDefinition.setCmHandleIds(targetIds)
386         return batchOperationDefinition
387     }
388 }