Performance Improvement: Use save batches of cmhandles
[cps.git] / cps-ncmp-service / src / test / groovy / org / onap / cps / ncmp / api / impl / NetworkCmProxyDataServiceImplSpec.groovy
1 /*
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
10  *
11  *        http://www.apache.org/licenses/LICENSE-2.0
12  *
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.
18  *
19  *  SPDX-License-Identifier: Apache-2.0
20  *  ============LICENSE_END=========================================================
21  */
22
23 package org.onap.cps.ncmp.api.impl
24
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
42
43 import java.util.stream.Collectors
44
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
49
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
59
60 class NetworkCmProxyDataServiceImplSpec extends Specification {
61
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)
71
72     def NO_TOPIC = null
73     def NO_REQUEST_ID = null
74     @Shared
75     def OPTIONS_PARAM = '(a=1,b=2)'
76     @Shared
77     def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'test-cm-handle-id')
78
79     def objectUnderTest = new NetworkCmProxyDataServiceImpl(
80             spiedJsonObjectMapper,
81             mockDmiDataOperations,
82             nullNetworkCmProxyDataServicePropertyHandler,
83             mockInventoryPersistence,
84             mockCmHandleQueries,
85             mockCpsCmHandlerQueryService,
86             mockLcmEventsCmHandleStateHandler,
87             mockCpsDataService)
88
89     def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
90
91     def dataNode = new DataNode(leaves: ['id': 'some-cm-handle', 'dmi-service-name': 'testDmiService'])
92
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) }
105     }
106
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(_, _, _, _, _)
116     }
117
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(
124                 'testCmHandle',
125                 'testResourceId',
126                 OPTIONS_PARAM,
127                 PASSTHROUGH_OPERATIONAL,
128                 NO_REQUEST_ID,
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',
132                 'testResourceId',
133                 OPTIONS_PARAM,
134                 NO_TOPIC,
135                 NO_REQUEST_ID)
136         then: 'DMI returns a json response'
137             response == 'dmi-response'
138     }
139
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',
146                 'testResourceId',
147                 OPTIONS_PARAM,
148                 PASSTHROUGH_RUNNING,
149                 NO_REQUEST_ID,
150                 NO_TOPIC) >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
151         when: 'get resource data is called'
152             def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
153                 'testResourceId',
154                 OPTIONS_PARAM,
155                 NO_TOPIC,
156                 NO_REQUEST_ID)
157         then: 'get resource data returns expected response'
158             response == '{dmi-response}'
159     }
160
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')
166     }
167
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(*_)
175     }
176
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
202
203     }
204
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(*_)
212     }
213
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' ]
225     }
226
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(*_)
234     }
235
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
252     }
253
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(_)
261     }
262
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) }
275     }
276
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.updateCmHandleStateBatch(_) >> {
287                 args ->
288                     {
289                         def cmHandleStatePerCmHandle = (args[0] as Map)
290                         cmHandleStatePerCmHandle.each {
291                             assert (it.key.id == 'test-cm-handle-id'
292                                     && it.value == CmHandleState.ADVISED)
293                         }
294                     }
295             }
296     }
297
298     def 'Execute cm handle id search'() {
299         given: 'valid CmHandleQueryApiParameters input'
300             def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
301             def conditionApiProperties = new ConditionApiProperties()
302             conditionApiProperties.conditionName = 'hasAllModules'
303             conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
304             cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
305         and: 'query cm handle method return with a data node list'
306             mockCpsCmHandlerQueryService.queryCmHandleIds(
307                     spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
308                     >> ['cm-handle-id-1']
309         when: 'execute cm handle search is called'
310             def result = objectUnderTest.executeCmHandleIdSearch(cmHandleQueryApiParameters)
311         then: 'result is the same collection as returned by the CPS Data Service'
312             assert result == ['cm-handle-id-1'] as Set
313     }
314
315     def 'Getting module definitions.'() {
316         when: 'get module definitions method is called with a valid cm handle ID'
317             objectUnderTest.getModuleDefinitionsByCmHandleId('some-cm-handle')
318         then: 'CPS module services is invoked once'
319             1 * mockInventoryPersistence.getModuleDefinitionsByCmHandleId('some-cm-handle')
320     }
321
322     def 'Execute cm handle search'() {
323         given: 'valid CmHandleQueryApiParameters input'
324             def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
325             def conditionApiProperties = new ConditionApiProperties()
326             conditionApiProperties.conditionName = 'hasAllModules'
327             conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
328             cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
329         and: 'query cm handle method return with a data node list'
330             mockCpsCmHandlerQueryService.queryCmHandles(
331                     spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
332                     >> [new NcmpServiceCmHandle(cmHandleId: 'cm-handle-id-1')]
333         when: 'execute cm handle search is called'
334             def result = objectUnderTest.executeCmHandleSearch(cmHandleQueryApiParameters)
335         then: 'result is the same collection as returned by the CPS Data Service'
336             assert result.stream().map(d -> d.cmHandleId).collect(Collectors.toSet()) == ['cm-handle-id-1'] as Set
337     }
338
339     def 'Set Cm Handle Data Sync Enabled Flag where data sync flag is  #scenario'() {
340         given: 'an existing cm handle composite state'
341             def compositeState = new CompositeState(cmHandleState: CmHandleState.READY, dataSyncEnabled: initialDataSyncEnabledFlag,
342                 dataStores: CompositeState.DataStores.builder()
343                     .operationalDataStore(CompositeState.Operational.builder()
344                         .dataStoreSyncState(initialDataSyncState)
345                         .build()).build())
346         and: 'get cm handle state returns the composite state for the given cm handle id'
347             mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
348         when: 'set data sync enabled is called with the data sync enabled flag set to #dataSyncEnabledFlag'
349             objectUnderTest.setDataSyncEnabled('some-cm-handle-id', dataSyncEnabledFlag)
350         then: 'the data sync enabled flag is set to #dataSyncEnabled'
351             compositeState.dataSyncEnabled == dataSyncEnabledFlag
352         and: 'the data store sync state is set to #expectedDataStoreSyncState'
353             compositeState.dataStores.operationalDataStore.dataStoreSyncState == expectedDataStoreSyncState
354         and: 'the cps data service to delete data nodes is invoked the expected number of times'
355             deleteDataNodeExpectedNumberOfInvocation * mockCpsDataService.deleteDataNode('NFP-Operational', 'some-cm-handle-id', '/netconf-state', _)
356         and: 'the inventory persistence service to update node leaves is called with the correct values'
357             saveCmHandleStateExpectedNumberOfInvocations * mockInventoryPersistence.saveCmHandleState('some-cm-handle-id', compositeState)
358         where: 'the following data sync enabled flag is used'
359             scenario                                              | dataSyncEnabledFlag | initialDataSyncEnabledFlag | initialDataSyncState               || expectedDataStoreSyncState         | deleteDataNodeExpectedNumberOfInvocation | saveCmHandleStateExpectedNumberOfInvocations
360             'enabled'                                             | true                | false                      | DataStoreSyncState.NONE_REQUESTED  || DataStoreSyncState.UNSYNCHRONIZED  | 0                                        | 1
361             'disabled'                                            | false               | true                       | DataStoreSyncState.UNSYNCHRONIZED  || DataStoreSyncState.NONE_REQUESTED  | 0                                        | 1
362             'disabled where sync-state is currently SYNCHRONIZED' | false               | true                       | DataStoreSyncState.SYNCHRONIZED    || DataStoreSyncState.NONE_REQUESTED  | 1                                        | 1
363             'is set to existing flag state'                       | true                | true                       | DataStoreSyncState.UNSYNCHRONIZED  || DataStoreSyncState.UNSYNCHRONIZED  | 0                                        | 0
364     }
365
366     def 'Set cm Handle Data Sync Enabled flag with following cm handle not in ready state exception' () {
367         given: 'a cm handle composite state'
368             def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED, dataSyncEnabled: false)
369         and: 'get cm handle state returns the composite state for the given cm handle id'
370             mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
371         when: 'set data sync enabled is called with the data sync enabled flag set to true'
372             objectUnderTest.setDataSyncEnabled('some-cm-handle-id', true)
373         then: 'the expected exception is thrown'
374             thrown(CpsException)
375         and: 'the inventory persistence service to update node leaves is not invoked'
376             0 * mockInventoryPersistence.saveCmHandleState(_, _)
377     }
378
379     def 'Get all cm handle IDs by DMI plugin identifier.' () {
380         given: 'cm handle queries service returns cm handles'
381             1 * mockCmHandleQueries.getCmHandlesByDmiPluginIdentifier('some-dmi-plugin-identifier')
382                     >> [new NcmpServiceCmHandle(cmHandleId: 'cm-handle-1'),
383                         new NcmpServiceCmHandle(cmHandleId: 'cm-handle-2')]
384         when: 'cm handle Ids are requested with dmi plugin identifier'
385             def result = objectUnderTest.getAllCmHandleIdsByDmiPluginIdentifier('some-dmi-plugin-identifier')
386         then: 'the result size is correct'
387             assert result.size() == 2
388         and: 'the result returns the correct details'
389             assert result.containsAll('cm-handle-1','cm-handle-2')
390     }
391
392     def dataStores() {
393         CompositeState.DataStores.builder()
394             .operationalDataStore(CompositeState.Operational.builder()
395                 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
396                 .lastSyncTime('some-timestamp').build()).build()
397     }
398 }