Merge "Add DMI to TrustLevel Cache During Registration"
[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.trustlevel.TrustLevel
30 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
31 import org.onap.cps.ncmp.api.inventory.CmHandleQueries
32 import org.onap.cps.ncmp.api.inventory.CmHandleState
33 import org.onap.cps.ncmp.api.inventory.CompositeState
34 import org.onap.cps.ncmp.api.inventory.InventoryPersistence
35 import org.onap.cps.ncmp.api.inventory.LockReasonCategory
36 import org.onap.cps.ncmp.api.inventory.DataStoreSyncState
37 import org.onap.cps.ncmp.api.models.DataOperationDefinition
38 import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters
39 import org.onap.cps.ncmp.api.models.CmHandleQueryServiceParameters
40 import org.onap.cps.ncmp.api.models.ConditionApiProperties
41 import org.onap.cps.ncmp.api.models.DmiPluginRegistration
42 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
43 import org.onap.cps.ncmp.api.models.DataOperationRequest
44 import org.onap.cps.spi.exceptions.CpsException
45 import org.onap.cps.spi.model.ConditionProperties
46 import spock.lang.Shared
47 import java.util.stream.Collectors
48 import org.onap.cps.utils.JsonObjectMapper
49 import com.fasterxml.jackson.databind.ObjectMapper
50 import org.onap.cps.api.CpsDataService
51 import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
52 import org.onap.cps.spi.FetchDescendantsOption
53 import org.onap.cps.spi.model.DataNode
54 import org.springframework.http.HttpStatus
55 import org.springframework.http.ResponseEntity
56 import spock.lang.Specification
57
58 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.OPERATIONAL
59 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL
60 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING
61 import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE
62 import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE
63
64 class NetworkCmProxyDataServiceImplSpec extends Specification {
65
66     def mockCpsDataService = Mock(CpsDataService)
67     def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
68     def mockDmiDataOperations = Mock(DmiDataOperations)
69     def nullNetworkCmProxyDataServicePropertyHandler = null
70     def mockInventoryPersistence = Mock(InventoryPersistence)
71     def mockCmHandleQueries = Mock(CmHandleQueries)
72     def mockDmiPluginRegistration = Mock(DmiPluginRegistration)
73     def mockCpsCmHandlerQueryService = Mock(NetworkCmProxyCmHandleQueryService)
74     def mockLcmEventsCmHandleStateHandler = Mock(LcmEventsCmHandleStateHandler)
75     def stubModuleSyncStartedOnCmHandles = Stub(IMap<String, Object>)
76     def stubTrustLevelPerDmiPlugin = Stub(IMap<String, TrustLevel>)
77
78     def NO_TOPIC = null
79     def NO_REQUEST_ID = null
80     @Shared
81     def OPTIONS_PARAM = '(a=1,b=2)'
82     @Shared
83     def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'test-cm-handle-id')
84
85     def objectUnderTest = new NetworkCmProxyDataServiceImpl(
86             spiedJsonObjectMapper,
87             mockDmiDataOperations,
88             nullNetworkCmProxyDataServicePropertyHandler,
89             mockInventoryPersistence,
90             mockCmHandleQueries,
91             mockCpsCmHandlerQueryService,
92             mockLcmEventsCmHandleStateHandler,
93             mockCpsDataService,
94             stubModuleSyncStartedOnCmHandles,
95             stubTrustLevelPerDmiPlugin)
96
97     def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
98
99     def dataNode = [new DataNode(leaves: ['id': 'some-cm-handle', 'dmi-service-name': 'testDmiService'])]
100
101     def 'Write resource data for pass-through running from DMI using POST.'() {
102         given: 'cpsDataService returns valid datanode'
103             mockDataNode()
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) }
112     }
113
114     def 'Get resource data for pass-through operational from DMI.'() {
115         given: 'cpsDataService returns valid data node'
116             mockDataNode()
117         and: 'get resource data from DMI is called'
118             mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_OPERATIONAL.datastoreName,'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID) >>
119                     new ResponseEntity<>('dmi-response', HttpStatus.OK)
120         when: 'get resource data operational for cm-handle is called'
121             def response = objectUnderTest.getResourceDataForCmHandle(PASSTHROUGH_OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID)
122         then: 'DMI returns a json response'
123             assert response == 'dmi-response'
124     }
125
126     def 'Get resource data for pass-through running from DMI.'() {
127         given: 'cpsDataService returns valid data node'
128             mockDataNode()
129         and: 'DMI returns valid response and data'
130             mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID) >>
131                     new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
132         when: 'get resource data is called'
133             def response = objectUnderTest.getResourceDataForCmHandle(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID)
134         then: 'get resource data returns expected response'
135             assert response == '{dmi-response}'
136     }
137
138     def 'Get resource data for operational (cached) data.'() {
139         given: 'CPS Data service returns some object(s)'
140             mockCpsDataService.getDataNodes(OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', FetchDescendantsOption.OMIT_DESCENDANTS) >> ['First Object', 'other Object']
141         when: 'get resource data is called'
142             def response = objectUnderTest.getResourceDataForCmHandle(OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', FetchDescendantsOption.OMIT_DESCENDANTS)
143         then: 'get resource data returns teh first object from the data service'
144             assert response == 'First Object'
145     }
146
147     def 'Execute (async) data operation for #datastoreName from DMI.'() {
148         given: 'cpsDataService returns valid data node'
149             def dataOperationRequest = getDataOperationRequest(datastoreName)
150         when: 'request resource data for data operation is called'
151             objectUnderTest.executeDataOperationForCmHandles('some topic', dataOperationRequest, 'requestId')
152         then: 'request resource data for data operation returns expected response'
153             1 * mockDmiDataOperations.requestResourceDataFromDmi('some topic', dataOperationRequest, 'requestId')
154         where: 'the following data stores are used'
155             datastoreName << [PASSTHROUGH_RUNNING.datastoreName, PASSTHROUGH_OPERATIONAL.datastoreName]
156     }
157
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')
163     }
164
165     def 'Get a cm handle.'() {
166         given: 'the system returns a yang modelled cm handle'
167             def dmiServiceName = 'some service name'
168             def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED,
169                 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.MODULE_SYNC_FAILED).details("lock details").build(),
170                 lastUpdateTime: 'some-timestamp',
171                 dataSyncEnabled: false,
172                 dataStores: dataStores())
173             def dmiProperties = [new YangModelCmHandle.Property('Book', 'Romance Novel')]
174             def publicProperties = [new YangModelCmHandle.Property('Public Book', 'Public Romance Novel')]
175             def yangModelCmHandle = new YangModelCmHandle(id: 'some-cm-handle', dmiServiceName: dmiServiceName,
176                 dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
177             1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
178         when: 'getting cm handle details for a given cm handle id from ncmp service'
179             def result = objectUnderTest.getNcmpServiceCmHandle('some-cm-handle')
180         then: 'the result is a ncmpServiceCmHandle'
181             result.class == NcmpServiceCmHandle.class
182         and: 'the cm handle contains the cm handle id'
183             result.cmHandleId == 'some-cm-handle'
184         and: 'the cm handle contains the DMI Properties'
185             result.dmiProperties ==[ Book:'Romance Novel' ]
186         and: 'the cm handle contains the public Properties'
187             result.publicProperties == [ "Public Book":'Public Romance Novel' ]
188         and: 'the cm handle contains the cm handle composite state'
189             result.compositeState == compositeState
190
191     }
192
193     def 'Get cm handle public properties'() {
194         given: 'a yang modelled cm handle'
195             def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
196             def publicProperties = [new YangModelCmHandle.Property('public prop', 'some public prop')]
197             def yangModelCmHandle = new YangModelCmHandle(id:'some-cm-handle', dmiServiceName: 'some service name', dmiProperties: dmiProperties, publicProperties: publicProperties)
198         and: 'the system returns this yang modelled cm handle'
199             1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
200         when: 'getting cm handle public properties for a given cm handle id from ncmp service'
201             def result = objectUnderTest.getCmHandlePublicProperties('some-cm-handle')
202         then: 'the result returns the correct data'
203             result == [ 'public prop' : 'some public prop' ]
204     }
205
206     def 'Execute cm handle id search for inventory'() {
207         given: 'a ConditionApiProperties object'
208             def conditionProperties = new ConditionProperties()
209             conditionProperties.conditionName = 'hasAllProperties'
210             conditionProperties.conditionParameters = [ [ 'some-key' : 'some-value' ] ]
211             def conditionServiceProps = new CmHandleQueryServiceParameters()
212             conditionServiceProps.cmHandleQueryParameters = [conditionProperties] as List<ConditionProperties>
213         and: 'the system returns an set of cmHandle ids'
214             mockCpsCmHandlerQueryService.queryCmHandleIdsForInventory(*_) >> [ 'cmHandle1', 'cmHandle2' ]
215         when: 'getting cm handle id set for a given dmi property'
216             def result = objectUnderTest.executeCmHandleIdSearchForInventory(conditionServiceProps)
217         then: 'the result returns the correct 2 elements'
218             assert result.size() == 2
219             assert result.contains('cmHandle1')
220             assert result.contains('cmHandle2')
221     }
222
223     def 'Get cm handle composite state'() {
224         given: 'a yang modelled cm handle'
225             def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED,
226                 lockReason: CompositeState.LockReason.builder().lockReasonCategory(LockReasonCategory.MODULE_SYNC_FAILED).details("lock details").build(),
227                 lastUpdateTime: 'some-timestamp',
228                 dataSyncEnabled: false,
229                 dataStores: dataStores())
230             def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
231             def publicProperties = [new YangModelCmHandle.Property('public prop', 'some public prop')]
232             def yangModelCmHandle = new YangModelCmHandle(id:'some-cm-handle', dmiServiceName: 'some service name', dmiProperties: dmiProperties, publicProperties: publicProperties, compositeState: compositeState)
233         and: 'the system returns this yang modelled cm handle'
234             1 * mockInventoryPersistence.getYangModelCmHandle('some-cm-handle') >> yangModelCmHandle
235         when: 'getting cm handle composite state for a given cm handle id from ncmp service'
236             def result = objectUnderTest.getCmHandleCompositeState('some-cm-handle')
237         then: 'the result returns the correct data'
238             result == compositeState
239     }
240
241     def 'Update resource data for pass-through running from dmi using POST #scenario DMI properties.'() {
242         given: 'cpsDataService returns valid datanode'
243             mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
244                 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
245         when: 'get resource data is called'
246             objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
247                 'testResourceId', UPDATE,
248                 '{some-json}', 'application/json')
249         then: 'DMI called with correct data'
250             1 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi('testCmHandle', 'testResourceId',
251                     UPDATE, '{some-json}', 'application/json')
252                 >> { new ResponseEntity<>(HttpStatus.OK) }
253     }
254
255     def 'Verify modules and create anchor params.'() {
256         given: 'dmi plugin registration return created cm handles'
257             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'service1', dmiModelPlugin: 'service1',
258                     dmiDataPlugin: 'service2')
259             dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
260             mockDmiPluginRegistration.getCreatedCmHandles() >> [ncmpServiceCmHandle]
261         when: 'parse and create cm handle in dmi registration then sync module'
262             objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(mockDmiPluginRegistration)
263         then: 'system persists the cm handle state'
264             1 * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(_) >> {
265                 args -> {
266                           def cmHandleStatePerCmHandle = (args[0] as Map)
267                           cmHandleStatePerCmHandle.each {
268                             assert it.key.id == 'test-cm-handle-id' && it.value == CmHandleState.ADVISED
269                           }
270                     }
271             }
272     }
273     
274     def 'Execute cm handle id search'() {
275         given: 'valid CmHandleQueryApiParameters input'
276             def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
277             def conditionApiProperties = new ConditionApiProperties()
278             conditionApiProperties.conditionName = 'hasAllModules'
279             conditionApiProperties.conditionParameters = [[moduleName: 'module-name-1']]
280             cmHandleQueryApiParameters.cmHandleQueryParameters = [conditionApiProperties]
281         and: 'query cm handle method return with a data node list'
282             mockCpsCmHandlerQueryService.queryCmHandleIds(
283                     spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
284                     >> ['cm-handle-id-1']
285         when: 'execute cm handle search is called'
286             def result = objectUnderTest.executeCmHandleIdSearch(cmHandleQueryApiParameters)
287         then: 'result is the same collection as returned by the CPS Data Service'
288             assert result == ['cm-handle-id-1']
289     }
290
291     def 'Getting module definitions.'() {
292         when: 'get module definitions method is called with a valid cm handle ID'
293             objectUnderTest.getModuleDefinitionsByCmHandleId('some-cm-handle')
294         then: 'CPS module services is invoked once'
295             1 * mockInventoryPersistence.getModuleDefinitionsByCmHandleId('some-cm-handle')
296     }
297
298     def 'Execute cm handle 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.queryCmHandles(
307                     spiedJsonObjectMapper.convertToValueType(cmHandleQueryApiParameters, CmHandleQueryServiceParameters.class))
308                     >> [new NcmpServiceCmHandle(cmHandleId: 'cm-handle-id-1')]
309         when: 'execute cm handle search is called'
310             def result = objectUnderTest.executeCmHandleSearch(cmHandleQueryApiParameters)
311         then: 'result is the same collection as returned by the CPS Data Service'
312             assert result.stream().map(d -> d.cmHandleId).collect(Collectors.toSet()) == ['cm-handle-id-1'] as Set
313     }
314
315     def 'Set Cm Handle Data Sync Enabled Flag where data sync flag is  #scenario'() {
316         given: 'an existing cm handle composite state'
317             def compositeState = new CompositeState(cmHandleState: CmHandleState.READY, dataSyncEnabled: initialDataSyncEnabledFlag,
318                 dataStores: CompositeState.DataStores.builder()
319                     .operationalDataStore(CompositeState.Operational.builder()
320                         .dataStoreSyncState(initialDataSyncState)
321                         .build()).build())
322         and: 'get cm handle state returns the composite state for the given cm handle id'
323             mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
324         when: 'set data sync enabled is called with the data sync enabled flag set to #dataSyncEnabledFlag'
325             objectUnderTest.setDataSyncEnabled('some-cm-handle-id', dataSyncEnabledFlag)
326         then: 'the data sync enabled flag is set to #dataSyncEnabled'
327             compositeState.dataSyncEnabled == dataSyncEnabledFlag
328         and: 'the data store sync state is set to #expectedDataStoreSyncState'
329             compositeState.dataStores.operationalDataStore.dataStoreSyncState == expectedDataStoreSyncState
330         and: 'the cps data service to delete data nodes is invoked the expected number of times'
331             deleteDataNodeExpectedNumberOfInvocation * mockCpsDataService.deleteDataNode('NFP-Operational', 'some-cm-handle-id', '/netconf-state', _)
332         and: 'the inventory persistence service to update node leaves is called with the correct values'
333             saveCmHandleStateExpectedNumberOfInvocations * mockInventoryPersistence.saveCmHandleState('some-cm-handle-id', compositeState)
334         where: 'the following data sync enabled flag is used'
335             scenario                                              | dataSyncEnabledFlag | initialDataSyncEnabledFlag | initialDataSyncState               || expectedDataStoreSyncState         | deleteDataNodeExpectedNumberOfInvocation | saveCmHandleStateExpectedNumberOfInvocations
336             'enabled'                                             | true                | false                      | DataStoreSyncState.NONE_REQUESTED  || DataStoreSyncState.UNSYNCHRONIZED  | 0                                        | 1
337             'disabled'                                            | false               | true                       | DataStoreSyncState.UNSYNCHRONIZED  || DataStoreSyncState.NONE_REQUESTED  | 0                                        | 1
338             'disabled where sync-state is currently SYNCHRONIZED' | false               | true                       | DataStoreSyncState.SYNCHRONIZED    || DataStoreSyncState.NONE_REQUESTED  | 1                                        | 1
339             'is set to existing flag state'                       | true                | true                       | DataStoreSyncState.UNSYNCHRONIZED  || DataStoreSyncState.UNSYNCHRONIZED  | 0                                        | 0
340     }
341
342     def 'Set cm Handle Data Sync Enabled flag with following cm handle not in ready state exception' () {
343         given: 'a cm handle composite state'
344             def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED, dataSyncEnabled: false)
345         and: 'get cm handle state returns the composite state for the given cm handle id'
346             mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
347         when: 'set data sync enabled is called with the data sync enabled flag set to true'
348             objectUnderTest.setDataSyncEnabled('some-cm-handle-id', true)
349         then: 'the expected exception is thrown'
350             thrown(CpsException)
351         and: 'the inventory persistence service to update node leaves is not invoked'
352             0 * mockInventoryPersistence.saveCmHandleState(_, _)
353     }
354
355     def 'Get all cm handle IDs by DMI plugin identifier.' () {
356         given: 'cm handle queries service returns cm handles'
357             1 * mockCmHandleQueries.getCmHandleIdsByDmiPluginIdentifier('some-dmi-plugin-identifier') >> ['cm-handle-1','cm-handle-2']
358         when: 'cm handle Ids are requested with dmi plugin identifier'
359             def result = objectUnderTest.getAllCmHandleIdsByDmiPluginIdentifier('some-dmi-plugin-identifier')
360         then: 'the result size is correct'
361             assert result.size() == 2
362         and: 'the result returns the correct details'
363             assert result.containsAll('cm-handle-1','cm-handle-2')
364     }
365
366     def dataStores() {
367         CompositeState.DataStores.builder()
368             .operationalDataStore(CompositeState.Operational.builder()
369                 .dataStoreSyncState(DataStoreSyncState.NONE_REQUESTED)
370                 .lastSyncTime('some-timestamp').build()).build()
371     }
372
373     def mockDataNode() {
374         mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
375                 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
376     }
377
378     def getDataOperationRequest(datastore) {
379         def dataOperationRequest = new DataOperationRequest()
380         def dataOperationDefinitions = new ArrayList()
381         dataOperationDefinitions.add(getDataOperationDefinition(datastore))
382         dataOperationRequest.setDataOperationDefinitions(dataOperationDefinitions)
383         return dataOperationRequest
384     }
385
386     def getDataOperationDefinition(datastore) {
387         def dataOperationDefinition = new DataOperationDefinition()
388         dataOperationDefinition.setOperation("read")
389         dataOperationDefinition.setOperationId("operational-12")
390         dataOperationDefinition.setDatastore(datastore)
391         def targetIds = new ArrayList()
392         targetIds.add("some-cm-handle")
393         dataOperationDefinition.setCmHandleIds(targetIds)
394         return dataOperationDefinition
395     }
396 }