ddb4bcd331931eff24a30935f0c927fd362ee051
[cps.git] /
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2022-2025 OpenInfra Foundation Europe. All rights reserved.
4  *  Modifications Copyright (C) 2022 Bell Canada
5  *  Modifications Copyright (C) 2024 TechMahindra Ltd.
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.impl.inventory
24
25 import com.fasterxml.jackson.databind.ObjectMapper
26 import com.hazelcast.map.IMap
27
28 import java.time.OffsetDateTime
29 import java.time.ZoneOffset
30 import java.time.format.DateTimeFormatter
31 import org.onap.cps.api.CpsAnchorService
32 import org.onap.cps.api.CpsDataService
33 import org.onap.cps.api.CpsModuleService
34 import org.onap.cps.api.exceptions.DataNodeNotFoundException
35 import org.onap.cps.api.exceptions.DataValidationException
36 import org.onap.cps.api.model.DataNode
37 import org.onap.cps.api.model.ModuleDefinition
38 import org.onap.cps.api.model.ModuleReference
39 import org.onap.cps.utils.CpsValidator
40 import org.onap.cps.ncmp.api.inventory.models.CompositeState
41 import org.onap.cps.ncmp.api.inventory.models.CmHandleState
42 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
43 import org.onap.cps.utils.ContentType
44 import org.onap.cps.utils.JsonObjectMapper
45 import spock.lang.Shared
46 import spock.lang.Specification
47
48 import static org.onap.cps.api.parameters.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
49 import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS
50 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DATASPACE_NAME
51 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
52 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT
53 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
54 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NO_TIMESTAMP
55
56 class InventoryPersistenceImplSpec extends Specification {
57
58     def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
59
60     def mockCpsDataService = Mock(CpsDataService)
61
62     def mockCpsModuleService = Mock(CpsModuleService)
63
64     def mockCpsAnchorService = Mock(CpsAnchorService)
65
66     def mockCpsValidator = Mock(CpsValidator)
67
68     def mockCmHandleIdPerAlternateId = Mock(IMap)
69
70     def objectUnderTest = new InventoryPersistenceImpl(mockCpsValidator, spiedJsonObjectMapper, mockCpsAnchorService, mockCpsModuleService, mockCpsDataService, mockCmHandleIdPerAlternateId)
71
72     def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
73             .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
74
75     def cmHandleId = 'some-cm-handle'
76     def alternateId = 'some-alternate-id'
77     def leaves = ["id":cmHandleId, "alternateId":alternateId,"dmi-service-name":"common service name","dmi-data-service-name":"data service name","dmi-model-service-name":"model service name"]
78     def xpath = "/dmi-registry/cm-handles[@id='some-cm-handle']"
79
80     def cmHandleId2 = 'another-cm-handle'
81     def alternateId2 = 'another-alternate-id'
82     def xpath2 = "/dmi-registry/cm-handles[@id='another-cm-handle']"
83
84     def dataNode = new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='myAdditionalProperty']", leaves: leaves)
85
86     @Shared
87     def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='myAdditionalProperty']", leaves: ["name":"myAdditionalProperty", "value":"myAdditionalValue"]),
88                                                       new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='myPublicProperty']", leaves: ["name":"myPublicProperty","value":"myPublicValue"])]
89
90     @Shared
91     def childDataNodesForCmHandleWithAdditionalProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='myAdditionalProperty']", leaves: ["name":"myAdditionalProperty", "value":"myAdditionalValue"])]
92
93     @Shared
94     def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='myPublicProperty']", leaves: ["name":"myPublicProperty","value":"myPublicValue"])]
95
96     @Shared
97     def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
98
99     def 'Retrieve CmHandle using datanode with #scenario.'() {
100         given: 'the cps data service returns a data node from the DMI registry'
101             def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
102             mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
103         when: 'retrieving the yang modelled cm handle'
104             def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
105         then: 'the result has the correct id and service names'
106             result.id == cmHandleId
107             result.dmiServiceName == 'common service name'
108             result.dmiDataServiceName == 'data service name'
109             result.dmiModelServiceName == 'model service name'
110         and: 'the expected additional properties'
111             result.additionalProperties.name == expectedAdditionalProperties
112         and: 'the expected public properties'
113             result.publicProperties.name == expectedPublicProperties
114         and: 'the state details are returned'
115             result.compositeState.cmHandleState == expectedCompositeState
116         and: 'the CM Handle ID is validated'
117             1 * mockCpsValidator.validateNameCharacters(cmHandleId)
118         where: 'the following parameters are used'
119             scenario                           | childDataNodes                                    || expectedAdditionalProperties || expectedPublicProperties || expectedCompositeState
120             'no properties'                    | []                                                || []                           || []                       || null
121             'additional and public properties' | childDataNodesForCmHandleWithAllProperties        || ["myAdditionalProperty"]     || ["myPublicProperty"]     || null
122             'just additional properties'       | childDataNodesForCmHandleWithAdditionalProperties || ["myAdditionalProperty"]     || []                       || null
123             'just public properties'           | childDataNodesForCmHandleWithPublicProperties     || []                           || ["myPublicProperty"]     || null
124             'with state details'               | childDataNodesForCmHandleWithState                || []                           || []                       || CmHandleState.ADVISED
125     }
126
127     def 'Handling missing service names as null.'() {
128         given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes'
129             def dataNode = new DataNode(childDataNodes:[], leaves: ['id':cmHandleId])
130             mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
131         when: 'retrieving the yang modelled cm handle'
132             def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
133         then: 'the service names are returned as null'
134             result.dmiServiceName == null
135             result.dmiDataServiceName == null
136             result.dmiModelServiceName == null
137         and: 'the CM Handle ID is validated'
138             1 * mockCpsValidator.validateNameCharacters(cmHandleId)
139     }
140
141     def 'Retrieve multiple YangModelCmHandles using cm handle ids'() {
142         given: 'the cps data service returns 2 data nodes from the DMI registry'
143             def dataNodes = [new DataNode(xpath: xpath, leaves: ['id': cmHandleId]), new DataNode(xpath: xpath2, leaves: ['id': cmHandleId2])]
144             mockCpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, [xpath, xpath2] , INCLUDE_ALL_DESCENDANTS) >> dataNodes
145         when: 'retrieving the yang modelled cm handles'
146             def results = objectUnderTest.getYangModelCmHandles([cmHandleId, cmHandleId2])
147         then: 'verify both have returned and cm handle Ids are correct'
148             assert results.size() == 2
149             assert results.id.containsAll([cmHandleId, cmHandleId2])
150     }
151
152     def 'YangModelCmHandles are not returned for invalid cm handle ids'() {
153         given: 'invalid cm handle id throws a data validation exception'
154             mockCpsValidator.validateNameCharacters('Invalid Cm Handle Id') >> {throw new DataValidationException('','')}
155         and: 'empty collection is returned as no valid cm handle ids are given'
156             mockCpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, [] , INCLUDE_ALL_DESCENDANTS) >> []
157         when: 'retrieving the yang modelled cm handles'
158             def results = objectUnderTest.getYangModelCmHandles(['Invalid Cm Handle Id'])
159         then: 'no YangModelCmHandle is returned'
160             assert results.size() == 0
161     }
162
163     def 'Get a Cm Handle Composite State'() {
164         given: 'a valid cm handle id'
165             def cmHandleId = 'Some-Cm-Handle'
166             def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
167         and: 'cps data service returns a valid data node'
168             mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
169                     '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', INCLUDE_ALL_DESCENDANTS) >> [dataNode]
170         when: 'get cm handle state is invoked'
171             def result = objectUnderTest.getCmHandleState(cmHandleId)
172         then: 'result has returned the correct cm handle state'
173             result.cmHandleState == CmHandleState.ADVISED
174         and: 'the CM Handle ID is validated'
175             1 * mockCpsValidator.validateNameCharacters(cmHandleId)
176     }
177
178     def 'Update Cm Handle with #scenario State'() {
179         given: 'a cm handle and a composite state'
180             def cmHandleId = 'Some-Cm-Handle'
181             def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
182         when: 'update cm handle state is invoked with the #scenario state'
183             objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
184         then: 'update node leaves is invoked with the correct params'
185             1 * mockCpsDataService.updateDataNodeAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime, ContentType.JSON)
186         where: 'the following states are used'
187             scenario    | cmHandleState          || expectedJsonData
188             'READY'     | CmHandleState.READY    || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
189             'LOCKED'    | CmHandleState.LOCKED   || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
190             'DELETING'  | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
191     }
192
193     def 'Update Cm Handles with #scenario States'() {
194         given: 'a map of cm handles composite states'
195             def compositeState1 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
196             def compositeState2 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
197         and: 'alternate id cache contains the given cm handle reference'
198             mockCmHandleIdPerAlternateId.containsKey(_) >> true
199         when: 'update cm handle state is invoked with the #scenario state'
200             def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2]
201             objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
202         then: 'update node leaves is invoked with the correct params'
203             1 * mockCpsDataService.updateDataNodesAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandlesJsonDataMap, _ as OffsetDateTime, ContentType.JSON)
204         where: 'the following states are used'
205             scenario    | cmHandleState          || cmHandlesJsonDataMap
206             'READY'     | CmHandleState.READY    || ['/dmi-registry/cm-handles[@id=\'Some-Cm-Handle1\']':'{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle2\']':'{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}']
207             'LOCKED'    | CmHandleState.LOCKED   || ['/dmi-registry/cm-handles[@id=\'Some-Cm-Handle1\']':'{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle2\']':'{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}']
208             'DELETING'  | CmHandleState.DELETING || ['/dmi-registry/cm-handles[@id=\'Some-Cm-Handle1\']':'{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle2\']':'{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}']
209     }
210
211     def 'Update cm handle states when #scenario in alternate id cache'() {
212         given: 'a map of cm handles composite states'
213             def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED, lastUpdateTime: formattedDateAndTime)
214             def cmHandleStateMap = ['some-cm-handle' : compositeState]
215         and: 'alternate id cache returns #scenario'
216             mockCmHandleIdPerAlternateId.containsKey(_) >> keyExists
217             mockCmHandleIdPerAlternateId.containsValue(_) >> valueExists
218         when: 'we update the state of a cm handle when #scenario'
219             objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
220         then: 'update node leaves is invoked correct number of times'
221             expectedCalls * mockCpsDataService.updateDataNodesAndDescendants(*_)
222         where: 'the following cm handle ids are used'
223             scenario            | keyExists | valueExists || expectedCalls
224             'id exists as key'  | true      | false       || 1
225             'id exists as value'| false     | true        || 1
226             'id does not exist' | false     | false       || 0
227
228     }
229
230     def 'Getting module definitions by module'() {
231         given: 'cps module service returns module definition for module name'
232             def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
233             mockCpsModuleService.getModuleDefinitionsByAnchorAndModule(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id', 'some-module', '2024-01-25') >> moduleDefinitions
234         when: 'get module definitions is invoked with module name'
235             def result = objectUnderTest.getModuleDefinitionsByCmHandleAndModule('some-cmHandle-Id', 'some-module', '2024-01-25')
236         then: 'returned result are the same module definitions as returned from module service'
237             assert result == moduleDefinitions
238         and: 'cm handle id and module name validated'
239             1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id', 'some-module')
240     }
241
242     def 'Getting module definitions with cm handle id'() {
243         given: 'cps module service returns module definitions for cm handle id'
244             def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
245             mockCpsModuleService.getModuleDefinitionsByAnchorName(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleDefinitions
246         when: 'get module definitions is invoked with cm handle id'
247             def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
248         then: 'the returned result are the same module definitions as returned from the module service'
249             assert result == moduleDefinitions
250     }
251
252     def 'Get module references'() {
253         given: 'cps module service returns a collection of module references'
254             def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
255             mockCpsModuleService.getYangResourcesModuleReferences(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleReferences
256         when: 'get yang resources module references by cmHandle is invoked'
257             def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
258         then: 'the returned result is a collection of module definitions'
259             assert result == moduleReferences
260         and: 'the CM Handle ID is validated'
261             1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id')
262     }
263
264     def 'Save Cmhandle'() {
265         given: 'cmHandle represented as Yang Model'
266             def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', additionalProperties: [], publicProperties: [])
267         when: 'the method to save cmhandle is called'
268             objectUnderTest.saveCmHandle(yangModelCmHandle)
269         then: 'the data service method to save list elements is called once'
270             1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT,
271                     _,null, ContentType.JSON) >> {
272                 args -> {
273                     assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
274                 }
275             }
276     }
277
278     def 'Save Multiple Cmhandles'() {
279         given: 'cm handles represented as Yang Model'
280             def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1')
281             def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2')
282         when: 'the cm handles are saved'
283             objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2])
284         then: 'CPS Data Service persists both cm handles as a batch'
285             1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
286                     NCMP_DMI_REGISTRY_PARENT, _,null, ContentType.JSON) >> {
287                 args -> {
288                     def jsonData = (args[3] as String)
289                     jsonData.contains('cmhandle1')
290                     jsonData.contains('cmhandle2')
291                 }
292             }
293     }
294
295     def 'Delete list or list elements'() {
296         when: 'the method to delete list or list elements is called'
297             objectUnderTest.deleteListOrListElement('sample xPath')
298         then: 'the data service method to save list elements is called once'
299             1 * mockCpsDataService.deleteListOrListElement(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath',null)
300     }
301
302     def 'Get data node via xPath'() {
303         when: 'the method to get data nodes is called'
304             objectUnderTest.getDataNode('sample xPath')
305         then: 'the data persistence service method to get data node is invoked once'
306             1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath', INCLUDE_ALL_DESCENDANTS)
307     }
308
309     def 'Get cmHandle data node'() {
310         given: 'expected xPath to get cmHandle data node'
311             def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']'
312         when: 'the method to get data nodes is called'
313             objectUnderTest.getCmHandleDataNodeByCmHandleId('sample cmHandleId', INCLUDE_ALL_DESCENDANTS)
314         then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
315             1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, expectedXPath, INCLUDE_ALL_DESCENDANTS)
316     }
317
318     def 'Get CM handle ids for CM Handles that has given module names'() {
319         when: 'the method to get cm handles is called'
320             objectUnderTest.getCmHandleReferencesWithGivenModules(['sample-module-name'], false)
321         then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
322             1 * mockCpsAnchorService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['sample-module-name'])
323     }
324
325     def 'Get Alternate Ids for CM Handles that has given module names'() {
326         given: 'cps anchor service returns a CM-handle ID for the given module name'
327             mockCpsAnchorService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['sample-module-name']) >> ['ch-1']
328         and: 'cps data service returns some data nodes for the given CM-handle ID'
329             def dataNodes = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='ch-1']", leaves: ['id': 'ch-1', 'alternate-id': 'alt-1'])]
330             mockCpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, ["/dmi-registry/cm-handles[@id='ch-1']"], OMIT_DESCENDANTS) >> dataNodes
331         when: 'the method to get cm-handle references by modules is called (outputting alternate IDs)'
332             def result = objectUnderTest.getCmHandleReferencesWithGivenModules(['sample-module-name'], true)
333         then: 'the result contains the correct alternate Id'
334             assert result == ['alt-1'] as Set
335     }
336
337     def 'Replace list content'() {
338         when: 'replace list content method is called with xpath and data nodes collection'
339             objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
340         then: 'the cps data service method to replace list content is invoked once with same parameters'
341             1 * mockCpsDataService.replaceListContent(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xpath', [new DataNode()], NO_TIMESTAMP);
342     }
343
344     def 'Delete data node via xPath'() {
345         when: 'Delete data node method is called with xpath as parameter'
346             objectUnderTest.deleteDataNode('sample dataNode xpath')
347         then: 'the cps data service method to delete data node is invoked once with the same xPath'
348             1 * mockCpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, 'sample dataNode xpath', NO_TIMESTAMP);
349     }
350
351     def 'Delete multiple data nodes via xPath'() {
352         when: 'Delete data nodes method is called with multiple xpaths as parameters'
353             objectUnderTest.deleteDataNodes(['xpath1', 'xpath2'])
354         then: 'the cps data service method to delete data nodes is invoked once with the same xPaths'
355             1 * mockCpsDataService.deleteDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, ['xpath1', 'xpath2'], NO_TIMESTAMP);
356     }
357
358     def 'CM handle exists'() {
359         given: 'data service returns a datanode with correct cm handle id'
360             mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, OMIT_DESCENDANTS) >> [dataNode]
361         expect: 'cm handle exists for given cm handle id'
362             assert true == objectUnderTest.isExistingCmHandleId(cmHandleId)
363     }
364
365     def 'CM handle does not exist, empty dataNode collection returned'() {
366         given: 'data service returns an empty datanode'
367             mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, OMIT_DESCENDANTS) >> []
368         expect: 'false is returned for non-existent cm handle'
369             assert false == objectUnderTest.isExistingCmHandleId(cmHandleId)
370     }
371
372     def 'CM handle does not exist, exception thrown'() {
373         given: 'data service throws an exception'
374             mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, "/dmi-registry/cm-handles[@id='non-existent-cm-handle']", OMIT_DESCENDANTS) >> {throw new DataNodeNotFoundException('','')}
375         expect: 'false is returned for non-existent cm handle'
376             assert false == objectUnderTest.isExistingCmHandleId('non-existent-cm-handle')
377     }
378 }