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