2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2022-2024 Nordix Foundation
4 * Modifications Copyright (C) 2022 Bell Canada
5 * Modifications Copyright (C) 2023 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
11 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 * SPDX-License-Identifier: Apache-2.0
20 * ============LICENSE_END=========================================================
23 package org.onap.cps.ncmp.api.impl.inventory
25 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
26 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
27 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT
28 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
29 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NO_TIMESTAMP
30 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
31 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
33 import com.fasterxml.jackson.databind.ObjectMapper
34 import org.onap.cps.api.CpsAnchorService
35 import org.onap.cps.api.CpsDataService
36 import org.onap.cps.api.CpsModuleService
37 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
38 import org.onap.cps.spi.CascadeDeleteAllowed
39 import org.onap.cps.spi.FetchDescendantsOption
40 import org.onap.cps.ncmp.api.impl.exception.NoAlternateIdParentFoundException
41 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
42 import org.onap.cps.spi.model.DataNode
43 import org.onap.cps.spi.model.ModuleDefinition
44 import org.onap.cps.spi.model.ModuleReference
45 import org.onap.cps.spi.utils.CpsValidator
46 import org.onap.cps.utils.JsonObjectMapper
47 import spock.lang.Shared
48 import spock.lang.Specification
49 import java.time.OffsetDateTime
50 import java.time.ZoneOffset
51 import java.time.format.DateTimeFormatter
53 class InventoryPersistenceImplSpec extends Specification {
55 def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
57 def mockCpsDataService = Mock(CpsDataService)
59 def mockCpsModuleService = Mock(CpsModuleService)
61 def mockCpsAnchorService = Mock(CpsAnchorService)
63 def mockCpsValidator = Mock(CpsValidator)
65 def mockCmHandleQueries = Mock(CmHandleQueries)
67 def objectUnderTest = new InventoryPersistenceImpl(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService,
68 mockCpsValidator, mockCpsAnchorService, mockCmHandleQueries)
70 def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
71 .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
73 def cmHandleId = 'some-cm-handle'
74 def leaves = ["id":cmHandleId,"dmi-service-name":"common service name","dmi-data-service-name":"data service name","dmi-model-service-name":"model service name"]
75 def xpath = "/dmi-registry/cm-handles[@id='some-cm-handle']"
77 def cmHandleId2 = 'another-cm-handle'
78 def xpath2 = "/dmi-registry/cm-handles[@id='another-cm-handle']"
81 def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"]),
82 new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
85 def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])]
88 def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
91 def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
93 def "Retrieve CmHandle using datanode with #scenario."() {
94 given: 'the cps data service returns a data node from the DMI registry'
95 def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
96 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
97 when: 'retrieving the yang modelled cm handle'
98 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
99 then: 'the result has the correct id and service names'
100 result.id == cmHandleId
101 result.dmiServiceName == 'common service name'
102 result.dmiDataServiceName == 'data service name'
103 result.dmiModelServiceName == 'model service name'
104 and: 'the expected DMI properties'
105 result.dmiProperties == expectedDmiProperties
106 result.publicProperties == expectedPublicProperties
107 and: 'the state details are returned'
108 result.compositeState.cmHandleState == expectedCompositeState
109 and: 'the CM Handle ID is validated'
110 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
111 where: 'the following parameters are used'
112 scenario | childDataNodes || expectedDmiProperties || expectedPublicProperties || expectedCompositeState
113 'no properties' | [] || [] || [] || null
114 'DMI and public properties' | childDataNodesForCmHandleWithAllProperties || [new YangModelCmHandle.Property("name1", "value1")] || [new YangModelCmHandle.Property("name2", "value2")] || null
115 'just DMI properties' | childDataNodesForCmHandleWithDMIProperties || [new YangModelCmHandle.Property("name1", "value1")] || [] || null
116 'just public properties' | childDataNodesForCmHandleWithPublicProperties || [] || [new YangModelCmHandle.Property("name2", "value2")] || null
117 'with state details' | childDataNodesForCmHandleWithState || [] || [] || CmHandleState.ADVISED
120 def "Handling missing service names as null."() {
121 given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes'
122 def dataNode = new DataNode(childDataNodes:[], leaves: ['id':cmHandleId])
123 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
124 when: 'retrieving the yang modelled cm handle'
125 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
126 then: 'the service names are returned as null'
127 result.dmiServiceName == null
128 result.dmiDataServiceName == null
129 result.dmiModelServiceName == null
130 and: 'the CM Handle ID is validated'
131 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
134 def "Retrieve multiple YangModelCmHandles"() {
135 given: 'the cps data service returns 2 data nodes from the DMI registry'
136 def dataNodes = [new DataNode(xpath: xpath, leaves: ['id': cmHandleId]), new DataNode(xpath: xpath2, leaves: ['id': cmHandleId2])]
137 mockCpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, [xpath, xpath2] , INCLUDE_ALL_DESCENDANTS) >> dataNodes
138 when: 'retrieving the yang modelled cm handle'
139 def results = objectUnderTest.getYangModelCmHandles([cmHandleId, cmHandleId2])
140 then: 'verify both have returned and cmhandleIds are correct'
141 assert results.size() == 2
142 assert results.id.containsAll([cmHandleId, cmHandleId2])
145 def 'Get a Cm Handle Composite State'() {
146 given: 'a valid cm handle id'
147 def cmHandleId = 'Some-Cm-Handle'
148 def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
149 and: 'cps data service returns a valid data node'
150 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
151 '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode]
152 when: 'get cm handle state is invoked'
153 def result = objectUnderTest.getCmHandleState(cmHandleId)
154 then: 'result has returned the correct cm handle state'
155 result.cmHandleState == CmHandleState.ADVISED
156 and: 'the CM Handle ID is validated'
157 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
160 def 'Update Cm Handle with #scenario State'() {
161 given: 'a cm handle and a composite state'
162 def cmHandleId = 'Some-Cm-Handle'
163 def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
164 when: 'update cm handle state is invoked with the #scenario state'
165 objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
166 then: 'update node leaves is invoked with the correct params'
167 1 * mockCpsDataService.updateDataNodeAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
168 where: 'the following states are used'
169 scenario | cmHandleState || expectedJsonData
170 'READY' | CmHandleState.READY || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
171 'LOCKED' | CmHandleState.LOCKED || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
172 'DELETING' | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
175 def 'Update Cm Handles with #scenario States'() {
176 given: 'a map of cm handles composite states'
177 def compositeState1 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
178 def compositeState2 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
179 when: 'update cm handle state is invoked with the #scenario state'
180 def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2]
181 objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
182 then: 'update node leaves is invoked with the correct params'
183 1 * mockCpsDataService.updateDataNodesAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandlesJsonDataMap, _ as OffsetDateTime)
184 where: 'the following states are used'
185 scenario | cmHandleState || cmHandlesJsonDataMap
186 '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"}}']
187 '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"}}']
188 '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"}}']
191 def 'Getting module definitions by module'() {
192 given: 'cps module service returns module definition for module name'
193 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
194 mockCpsModuleService.getModuleDefinitionsByAnchorAndModule(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id', 'some-module', '2024-01-25') >> moduleDefinitions
195 when: 'get module definitions is invoked with module name'
196 def result = objectUnderTest.getModuleDefinitionsByCmHandleAndModule('some-cmHandle-Id', 'some-module', '2024-01-25')
197 then: 'returned result are the same module definitions as returned from module service'
198 assert result == moduleDefinitions
199 and: 'cm handle id and module name validated'
200 1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id', 'some-module')
203 def 'Getting module definitions with cm handle id'() {
204 given: 'cps module service returns module definitions for cm handle id'
205 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
206 mockCpsModuleService.getModuleDefinitionsByAnchorName(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleDefinitions
207 when: 'get module definitions is invoked with cm handle id'
208 def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
209 then: 'the returned result are the same module definitions as returned from the module service'
210 assert result == moduleDefinitions
213 def 'Get module references'() {
214 given: 'cps module service returns a collection of module references'
215 def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
216 mockCpsModuleService.getYangResourcesModuleReferences(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleReferences
217 when: 'get yang resources module references by cmHandle is invoked'
218 def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
219 then: 'the returned result is a collection of module definitions'
220 assert result == moduleReferences
221 and: 'the CM Handle ID is validated'
222 1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id')
225 def 'Save Cmhandle'() {
226 given: 'cmHandle represented as Yang Model'
227 def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: [])
228 when: 'the method to save cmhandle is called'
229 objectUnderTest.saveCmHandle(yangModelCmHandle)
230 then: 'the data service method to save list elements is called once'
231 1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT,
234 assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
239 def 'Save Multiple Cmhandles'() {
240 given: 'cm handles represented as Yang Model'
241 def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1')
242 def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2')
243 when: 'the cm handles are saved'
244 objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2])
245 then: 'CPS Data Service persists both cm handles as a batch'
246 1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
247 NCMP_DMI_REGISTRY_PARENT, _,null) >> {
249 def jsonData = (args[3] as String)
250 jsonData.contains('cmhandle1')
251 jsonData.contains('cmhandle2')
256 def 'Delete list or list elements'() {
257 when: 'the method to delete list or list elements is called'
258 objectUnderTest.deleteListOrListElement('sample xPath')
259 then: 'the data service method to save list elements is called once'
260 1 * mockCpsDataService.deleteListOrListElement(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath',null)
263 def 'Delete schema set with a valid schema set name'() {
264 when: 'the method to delete schema set is called with valid schema set name'
265 objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
266 then: 'the module service to delete schemaSet is invoked once'
267 1 * mockCpsModuleService.deleteSchemaSet(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
268 and: 'the schema set name is validated'
269 1 * mockCpsValidator.validateNameCharacters('validSchemaSetName')
272 def 'Delete multiple schema sets with valid schema set names'() {
273 when: 'the method to delete schema sets is called with valid schema set names'
274 objectUnderTest.deleteSchemaSetsWithCascade(['validSchemaSetName1', 'validSchemaSetName2'])
275 then: 'the module service to delete schema sets is invoked once'
276 1 * mockCpsModuleService.deleteSchemaSetsWithCascade(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['validSchemaSetName1', 'validSchemaSetName2'])
277 and: 'the schema set names are validated'
278 1 * mockCpsValidator.validateNameCharacters(['validSchemaSetName1', 'validSchemaSetName2'])
281 def 'Get data node via xPath'() {
282 when: 'the method to get data nodes is called'
283 objectUnderTest.getDataNode('sample xPath')
284 then: 'the data persistence service method to get data node is invoked once'
285 1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath', INCLUDE_ALL_DESCENDANTS)
288 def 'Get cmHandle data node'() {
289 given: 'expected xPath to get cmHandle data node'
290 def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']'
291 when: 'the method to get data nodes is called'
292 objectUnderTest.getCmHandleDataNodeByCmHandleId('sample cmHandleId')
293 then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
294 1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, expectedXPath, INCLUDE_ALL_DESCENDANTS)
297 def 'Get cm handle data node'() {
298 given: 'expected xPath to get cmHandle data node'
299 def expectedXPath = '/dmi-registry/cm-handles[@alternate-id=\'alternate id\']'
300 and: 'query service is invoked with expected xpath'
301 mockCmHandleQueries.queryNcmpRegistryByCpsPath(expectedXPath, OMIT_DESCENDANTS) >> [new DataNode()]
302 expect: 'getting the cm handle data node'
303 assert objectUnderTest.getCmHandleDataNodeByAlternateId('alternate id') == new DataNode()
306 def 'Find cm handle parent data node using alternate ids'() {
307 given: 'cm handle in the registry with alternateId /a/b'
308 def matchingCpsPath = "/dmi-registry/cm-handles[@alternate-id='/a/b']"
309 mockCmHandleQueries.queryNcmpRegistryByCpsPath(matchingCpsPath, OMIT_DESCENDANTS) >> [new DataNode()]
310 and: 'no other cm handle'
311 mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> []
312 expect: 'querying for alternate id a matching result found'
313 assert objectUnderTest.getCmHandleDataNodeByLongestMatchAlternateId(alternateId, '/') != null
314 where: 'the following parameters are used'
315 scenario | alternateId
316 'exact match' | '/a/b'
317 'exact match with trailing separator' | '/a/b/'
318 'child match' | '/a/b/c'
321 def 'Find cm handle parent data node using alternate ids mismatches'() {
322 given: 'cm handle in the registry with alternateId'
323 def matchingCpsPath = "/dmi-registry/cm-handles[@alternate-id='${cpsPath}]"
324 mockCmHandleQueries.queryNcmpRegistryByCpsPath(matchingCpsPath, OMIT_DESCENDANTS) >> [new DataNode()]
325 and: 'no other cm handle'
326 mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> []
327 when: 'attempt to find alternateId'
328 objectUnderTest.getCmHandleDataNodeByLongestMatchAlternateId(alternateId, '/')
329 then: 'no alternate id found exception thrown'
330 def thrown = thrown(NoAlternateIdParentFoundException)
331 and: 'the exception has the relevant details from the error response'
332 assert thrown.message == 'No matching (parent) cm handle found using alternate ids'
333 assert thrown.details == 'cannot find a datanode with alternate id ' + alternateId
334 where: 'the following parameters are used'
335 scenario | alternateId | cpsPath
336 'no match for parent only' | '/a' | '/a/b'
337 'no match at all' | '/x/y/z' | '/a/b'
338 'no match with trailing separator' | '/c/d/' | '/c/d'
341 def 'Attempt to get non existing cm handle data node by alternate id'() {
342 given: 'query service is invoked and returns empty collection of data nodes'
343 mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> []
344 when: 'getting the cm handle data node'
345 objectUnderTest.getCmHandleDataNodeByAlternateId('alternate id')
346 then: 'no data found exception thrown'
347 def thrownException = thrown(DataNodeNotFoundException)
348 assert thrownException.getMessage().contains('DataNode not found')
351 def 'Get CM handles that has given module names'() {
352 when: 'the method to get cm handles is called'
353 objectUnderTest.getCmHandleIdsWithGivenModules(['sample-module-name'])
354 then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
355 1 * mockCpsAnchorService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['sample-module-name'])
358 def 'Replace list content'() {
359 when: 'replace list content method is called with xpath and data nodes collection'
360 objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
361 then: 'the cps data service method to replace list content is invoked once with same parameters'
362 1 * mockCpsDataService.replaceListContent(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xpath', [new DataNode()], NO_TIMESTAMP);
365 def 'Delete data node via xPath'() {
366 when: 'Delete data node method is called with xpath as parameter'
367 objectUnderTest.deleteDataNode('sample dataNode xpath')
368 then: 'the cps data service method to delete data node is invoked once with the same xPath'
369 1 * mockCpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, 'sample dataNode xpath', NO_TIMESTAMP);
372 def 'Delete multiple data nodes via xPath'() {
373 when: 'Delete data nodes method is called with multiple xpaths as parameters'
374 objectUnderTest.deleteDataNodes(['xpath1', 'xpath2'])
375 then: 'the cps data service method to delete data nodes is invoked once with the same xPaths'
376 1 * mockCpsDataService.deleteDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, ['xpath1', 'xpath2'], NO_TIMESTAMP);