2  *  ============LICENSE_START=======================================================
 
   3  *  Copyright (C) 2022-2024 Nordix Foundation
 
   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
 
  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.impl.inventory
 
  25 import com.fasterxml.jackson.databind.ObjectMapper
 
  26 import org.onap.cps.api.CpsAnchorService
 
  27 import org.onap.cps.api.CpsDataService
 
  28 import org.onap.cps.api.CpsModuleService
 
  29 import org.onap.cps.api.exceptions.DataNodeNotFoundException
 
  30 import org.onap.cps.api.exceptions.DataValidationException
 
  31 import org.onap.cps.impl.utils.CpsValidator
 
  32 import org.onap.cps.ncmp.api.exceptions.CmHandleNotFoundException
 
  33 import org.onap.cps.ncmp.api.inventory.models.CompositeState
 
  34 import org.onap.cps.ncmp.impl.inventory.models.CmHandleState
 
  35 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
 
  36 import org.onap.cps.ncmp.impl.utils.YangDataConverter
 
  37 import org.onap.cps.api.parameters.CascadeDeleteAllowed
 
  38 import org.onap.cps.api.parameters.FetchDescendantsOption
 
  39 import org.onap.cps.api.model.DataNode
 
  40 import org.onap.cps.api.model.ModuleDefinition
 
  41 import org.onap.cps.api.model.ModuleReference
 
  42 import org.onap.cps.utils.ContentType
 
  43 import org.onap.cps.utils.JsonObjectMapper
 
  44 import spock.lang.Shared
 
  45 import spock.lang.Specification
 
  47 import java.time.OffsetDateTime
 
  48 import java.time.ZoneOffset
 
  49 import java.time.format.DateTimeFormatter
 
  51 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DATASPACE_NAME
 
  52 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
 
  53 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT
 
  54 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
 
  55 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NO_TIMESTAMP
 
  56 import static org.onap.cps.api.parameters.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
 
  57 import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS
 
  59 class InventoryPersistenceImplSpec extends Specification {
 
  61     def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
 
  63     def mockCpsDataService = Mock(CpsDataService)
 
  65     def mockCpsModuleService = Mock(CpsModuleService)
 
  67     def mockCpsAnchorService = Mock(CpsAnchorService)
 
  69     def mockCpsValidator = Mock(CpsValidator)
 
  71     def mockCmHandleQueries = Mock(CmHandleQueryService)
 
  73     def mockYangDataConverter = Mock(YangDataConverter)
 
  75     def objectUnderTest = new InventoryPersistenceImpl(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService,
 
  76             mockCpsValidator, mockCpsAnchorService, mockCmHandleQueries)
 
  78     def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
 
  79             .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
 
  81     def cmHandleId = 'some-cm-handle'
 
  82     def alternateId = 'some-alternate-id'
 
  83     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"]
 
  84     def xpath = "/dmi-registry/cm-handles[@id='some-cm-handle']"
 
  86     def cmHandleId2 = 'another-cm-handle'
 
  87     def alternateId2 = 'another-alternate-id'
 
  88     def xpath2 = "/dmi-registry/cm-handles[@id='another-cm-handle']"
 
  90     def dataNode = new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: leaves)
 
  93     def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"]),
 
  94                                                       new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
 
  97     def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])]
 
 100     def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
 
 103     def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
 
 105     def 'Retrieve CmHandle using datanode with #scenario.'() {
 
 106         given: 'the cps data service returns a data node from the DMI registry'
 
 107             def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
 
 108             mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
 
 109         when: 'retrieving the yang modelled cm handle'
 
 110             def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
 
 111         then: 'the result has the correct id and service names'
 
 112             result.id == cmHandleId
 
 113             result.dmiServiceName == 'common service name'
 
 114             result.dmiDataServiceName == 'data service name'
 
 115             result.dmiModelServiceName == 'model service name'
 
 116         and: 'the expected DMI properties'
 
 117             result.dmiProperties == expectedDmiProperties
 
 118             result.publicProperties == expectedPublicProperties
 
 119         and: 'the state details are returned'
 
 120             result.compositeState.cmHandleState == expectedCompositeState
 
 121         and: 'the CM Handle ID is validated'
 
 122             1 * mockCpsValidator.validateNameCharacters(cmHandleId)
 
 123         where: 'the following parameters are used'
 
 124             scenario                    | childDataNodes                                || expectedDmiProperties                               || expectedPublicProperties                              || expectedCompositeState
 
 125             'no properties'             | []                                            || []                                                  || []                                                    || null
 
 126             'DMI and public properties' | childDataNodesForCmHandleWithAllProperties    || [new YangModelCmHandle.Property("name1", "value1")] || [new YangModelCmHandle.Property("name2", "value2")]   || null
 
 127             'just DMI properties'       | childDataNodesForCmHandleWithDMIProperties    || [new YangModelCmHandle.Property("name1", "value1")] || []                                                    || null
 
 128             'just public properties'    | childDataNodesForCmHandleWithPublicProperties || []                                                  || [new YangModelCmHandle.Property("name2", "value2")]   || null
 
 129             'with state details'        | childDataNodesForCmHandleWithState            || []                                                  || []                                                    || CmHandleState.ADVISED
 
 132     def 'Handling missing service names as null.'() {
 
 133         given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes'
 
 134             def dataNode = new DataNode(childDataNodes:[], leaves: ['id':cmHandleId])
 
 135             mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
 
 136         when: 'retrieving the yang modelled cm handle'
 
 137             def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
 
 138         then: 'the service names are returned as null'
 
 139             result.dmiServiceName == null
 
 140             result.dmiDataServiceName == null
 
 141             result.dmiModelServiceName == null
 
 142         and: 'the CM Handle ID is validated'
 
 143             1 * mockCpsValidator.validateNameCharacters(cmHandleId)
 
 146     def 'Retrieve multiple YangModelCmHandles using cm handle ids'() {
 
 147         given: 'the cps data service returns 2 data nodes from the DMI registry'
 
 148             def dataNodes = [new DataNode(xpath: xpath, leaves: ['id': cmHandleId]), new DataNode(xpath: xpath2, leaves: ['id': cmHandleId2])]
 
 149             mockCpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, [xpath, xpath2] , INCLUDE_ALL_DESCENDANTS) >> dataNodes
 
 150         when: 'retrieving the yang modelled cm handles'
 
 151             def results = objectUnderTest.getYangModelCmHandles([cmHandleId, cmHandleId2])
 
 152         then: 'verify both have returned and cm handle Ids are correct'
 
 153             assert results.size() == 2
 
 154             assert results.id.containsAll([cmHandleId, cmHandleId2])
 
 157     def 'YangModelCmHandles are not returned for invalid cm handle ids'() {
 
 158         given: 'invalid cm handle id throws a data validation exception'
 
 159             mockCpsValidator.validateNameCharacters('Invalid Cm Handle Id') >> {throw new DataValidationException('','')}
 
 160         and: 'empty collection is returned as no valid cm handle ids are given'
 
 161             mockCpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, [] , INCLUDE_ALL_DESCENDANTS) >> []
 
 162         when: 'retrieving the yang modelled cm handles'
 
 163             def results = objectUnderTest.getYangModelCmHandles(['Invalid Cm Handle Id'])
 
 164         then: 'no YangModelCmHandle is returned'
 
 165             assert results.size() == 0
 
 168     def "Retrieve multiple YangModelCmHandles using cm handle references"() {
 
 169         given: 'the cps data service returns 2 data nodes from the DMI registry'
 
 170             def dataNodes = [new DataNode(xpath: xpath, leaves: ['id': cmHandleId, 'alternate-id':alternateId]), new DataNode(xpath: xpath2, leaves: ['id': cmHandleId2,'alternate-id':alternateId2])]
 
 171             mockCmHandleQueries.queryNcmpRegistryByCpsPath(_, INCLUDE_ALL_DESCENDANTS) >> dataNodes
 
 172         when: 'retrieving the yang modelled cm handle'
 
 173             def results = objectUnderTest.getYangModelCmHandlesFromCmHandleReferences([cmHandleId, cmHandleId2])
 
 174         then: 'verify both have returned and cmhandleIds are correct'
 
 175             assert results.size() == 2
 
 176             assert results.id.containsAll([cmHandleId, cmHandleId2])
 
 179     def 'Get a Cm Handle Composite State'() {
 
 180         given: 'a valid cm handle id'
 
 181             def cmHandleId = 'Some-Cm-Handle'
 
 182             def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
 
 183         and: 'cps data service returns a valid data node'
 
 184             mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
 
 185                     '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode]
 
 186         when: 'get cm handle state is invoked'
 
 187             def result = objectUnderTest.getCmHandleState(cmHandleId)
 
 188         then: 'result has returned the correct cm handle state'
 
 189             result.cmHandleState == CmHandleState.ADVISED
 
 190         and: 'the CM Handle ID is validated'
 
 191             1 * mockCpsValidator.validateNameCharacters(cmHandleId)
 
 194     def 'Update Cm Handle with #scenario State'() {
 
 195         given: 'a cm handle and a composite state'
 
 196             def cmHandleId = 'Some-Cm-Handle'
 
 197             def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
 
 198         when: 'update cm handle state is invoked with the #scenario state'
 
 199             objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
 
 200         then: 'update node leaves is invoked with the correct params'
 
 201             1 * mockCpsDataService.updateDataNodeAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime, ContentType.JSON)
 
 202         where: 'the following states are used'
 
 203             scenario    | cmHandleState          || expectedJsonData
 
 204             'READY'     | CmHandleState.READY    || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
 
 205             'LOCKED'    | CmHandleState.LOCKED   || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
 
 206             'DELETING'  | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
 
 209     def 'Update Cm Handles with #scenario States'() {
 
 210         given: 'a map of cm handles composite states'
 
 211             def compositeState1 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
 
 212             def compositeState2 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
 
 213         when: 'update cm handle state is invoked with the #scenario state'
 
 214             def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2]
 
 215             objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
 
 216         then: 'update node leaves is invoked with the correct params'
 
 217             1 * mockCpsDataService.updateDataNodesAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandlesJsonDataMap, _ as OffsetDateTime, ContentType.JSON)
 
 218         where: 'the following states are used'
 
 219             scenario    | cmHandleState          || cmHandlesJsonDataMap
 
 220             '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"}}']
 
 221             '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"}}']
 
 222             '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"}}']
 
 225     def 'Getting module definitions by module'() {
 
 226         given: 'cps module service returns module definition for module name'
 
 227             def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
 
 228             mockCpsModuleService.getModuleDefinitionsByAnchorAndModule(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id', 'some-module', '2024-01-25') >> moduleDefinitions
 
 229         when: 'get module definitions is invoked with module name'
 
 230             def result = objectUnderTest.getModuleDefinitionsByCmHandleAndModule('some-cmHandle-Id', 'some-module', '2024-01-25')
 
 231         then: 'returned result are the same module definitions as returned from module service'
 
 232             assert result == moduleDefinitions
 
 233         and: 'cm handle id and module name validated'
 
 234             1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id', 'some-module')
 
 237     def 'Getting module definitions with cm handle id'() {
 
 238         given: 'cps module service returns module definitions for cm handle id'
 
 239             def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
 
 240             mockCpsModuleService.getModuleDefinitionsByAnchorName(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleDefinitions
 
 241         when: 'get module definitions is invoked with cm handle id'
 
 242             def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
 
 243         then: 'the returned result are the same module definitions as returned from the module service'
 
 244             assert result == moduleDefinitions
 
 247     def 'Get module references'() {
 
 248         given: 'cps module service returns a collection of module references'
 
 249             def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
 
 250             mockCpsModuleService.getYangResourcesModuleReferences(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleReferences
 
 251         when: 'get yang resources module references by cmHandle is invoked'
 
 252             def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
 
 253         then: 'the returned result is a collection of module definitions'
 
 254             assert result == moduleReferences
 
 255         and: 'the CM Handle ID is validated'
 
 256             1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id')
 
 259     def 'Save Cmhandle'() {
 
 260         given: 'cmHandle represented as Yang Model'
 
 261             def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: [])
 
 262         when: 'the method to save cmhandle is called'
 
 263             objectUnderTest.saveCmHandle(yangModelCmHandle)
 
 264         then: 'the data service method to save list elements is called once'
 
 265             1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT,
 
 266                     _,null, ContentType.JSON) >> {
 
 268                     assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
 
 273     def 'Save Multiple Cmhandles'() {
 
 274         given: 'cm handles represented as Yang Model'
 
 275             def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1')
 
 276             def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2')
 
 277         when: 'the cm handles are saved'
 
 278             objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2])
 
 279         then: 'CPS Data Service persists both cm handles as a batch'
 
 280             1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
 
 281                     NCMP_DMI_REGISTRY_PARENT, _,null, ContentType.JSON) >> {
 
 283                     def jsonData = (args[3] as String)
 
 284                     jsonData.contains('cmhandle1')
 
 285                     jsonData.contains('cmhandle2')
 
 290     def 'Delete list or list elements'() {
 
 291         when: 'the method to delete list or list elements is called'
 
 292             objectUnderTest.deleteListOrListElement('sample xPath')
 
 293         then: 'the data service method to save list elements is called once'
 
 294             1 * mockCpsDataService.deleteListOrListElement(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath',null)
 
 297     def 'Delete schema set with a valid schema set name'() {
 
 298         when: 'the method to delete schema set is called with valid schema set name'
 
 299             objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
 
 300         then: 'the module service to delete schemaSet is invoked once'
 
 301             1 * mockCpsModuleService.deleteSchemaSet(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
 
 302         and: 'the schema set name is validated'
 
 303             1 * mockCpsValidator.validateNameCharacters('validSchemaSetName')
 
 306     def 'Delete multiple schema sets with valid schema set names'() {
 
 307         when: 'the method to delete schema sets is called with valid schema set names'
 
 308             objectUnderTest.deleteSchemaSetsWithCascade(['validSchemaSetName1', 'validSchemaSetName2'])
 
 309         then: 'the module service to delete schema sets is invoked once'
 
 310             1 * mockCpsModuleService.deleteSchemaSetsWithCascade(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['validSchemaSetName1', 'validSchemaSetName2'])
 
 311         and: 'the schema set names are validated'
 
 312             1 * mockCpsValidator.validateNameCharacters(['validSchemaSetName1', 'validSchemaSetName2'])
 
 315     def 'Get data node via xPath'() {
 
 316         when: 'the method to get data nodes is called'
 
 317             objectUnderTest.getDataNode('sample xPath')
 
 318         then: 'the data persistence service method to get data node is invoked once'
 
 319             1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath', INCLUDE_ALL_DESCENDANTS)
 
 322     def 'Get cmHandle data node'() {
 
 323         given: 'expected xPath to get cmHandle data node'
 
 324             def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']'
 
 325         when: 'the method to get data nodes is called'
 
 326             objectUnderTest.getCmHandleDataNodeByCmHandleId('sample cmHandleId')
 
 327         then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
 
 328             1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, expectedXPath, INCLUDE_ALL_DESCENDANTS)
 
 331     def 'Get yang model cm handle by alternate id'() {
 
 332         given: 'expected xPath to get cmHandle data node'
 
 333             def expectedXPath = '/dmi-registry/cm-handles[@alternate-id=\'alternate id\']'
 
 334             def expectedDataNode = new DataNode(xpath: expectedXPath, leaves: [id: 'id', alternateId: 'alternate id'])
 
 335         and: 'query service is invoked with expected xpath'
 
 336             mockCmHandleQueries.queryNcmpRegistryByCpsPath(expectedXPath, OMIT_DESCENDANTS) >> [expectedDataNode]
 
 337             mockYangDataConverter.toYangModelCmHandle(expectedDataNode) >> new YangModelCmHandle(id: 'id')
 
 338         expect: 'getting the yang model cm handle'
 
 339             assert objectUnderTest.getYangModelCmHandleByAlternateId('alternate id') == new YangModelCmHandle(id: 'id')
 
 342     def 'Attempt to get non existing yang model cm handle by alternate id'() {
 
 343         given: 'query service is invoked and returns empty collection of data nodes'
 
 344             mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> []
 
 345         when: 'getting the yang model cm handle'
 
 346             objectUnderTest.getYangModelCmHandleByAlternateId('alternate id')
 
 347         then: 'no data found exception thrown'
 
 348             def thrownException = thrown(CmHandleNotFoundException)
 
 349             assert thrownException.getMessage().contains('Cm handle not found')
 
 350             assert thrownException.getDetails().contains('No cm handles found with reference alternate id')
 
 353     def 'Get multiple yang model cm handles by alternate ids #scenario'() {
 
 354         when: 'getting the yang model cm handle with a empty/populated collection of alternate Ids'
 
 355             objectUnderTest.getYangModelCmHandleByAlternateIds(alternateIdCollection)
 
 356         then: 'query service invoked when needed'
 
 357             expectedInvocations * mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> [dataNode]
 
 358         where: 'collections are either empty or populated with alternate ids'
 
 359             scenario               | alternateIdCollection || expectedInvocations
 
 360             'empty collection'     | []                    || 0
 
 361             'populated collection' | ['alt']               || 1
 
 364     def 'Get CM handle ids for CM Handles that has given module names'() {
 
 365         when: 'the method to get cm handles is called'
 
 366             objectUnderTest.getCmHandleReferencesWithGivenModules(['sample-module-name'], false)
 
 367         then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
 
 368             1 * mockCpsAnchorService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['sample-module-name'])
 
 371     def 'Get Alternate Ids for CM Handles that has given module names'() {
 
 372         given: 'A Collection of data nodes'
 
 373             def dataNodes = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='ch-1']", leaves: ['id': 'ch-1', 'alternate-id': 'alt-1'])]
 
 374         when: 'the methods to get dataNodes is called and returns correct values'
 
 375             mockCpsAnchorService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['sample-module-name']) >> ['ch-1']
 
 376             mockCpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, ["/dmi-registry/cm-handles[@id='ch-1']"], INCLUDE_ALL_DESCENDANTS) >> dataNodes
 
 377         and: 'the method returns a result'
 
 378             def result = objectUnderTest.getCmHandleReferencesWithGivenModules(['sample-module-name'], true)
 
 379         then: 'the result contains the correct alternate Id'
 
 380             assert result == ['alt-1'] as HashSet
 
 383     def 'Replace list content'() {
 
 384         when: 'replace list content method is called with xpath and data nodes collection'
 
 385             objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
 
 386         then: 'the cps data service method to replace list content is invoked once with same parameters'
 
 387             1 * mockCpsDataService.replaceListContent(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xpath', [new DataNode()], NO_TIMESTAMP);
 
 390     def 'Delete data node via xPath'() {
 
 391         when: 'Delete data node method is called with xpath as parameter'
 
 392             objectUnderTest.deleteDataNode('sample dataNode xpath')
 
 393         then: 'the cps data service method to delete data node is invoked once with the same xPath'
 
 394             1 * mockCpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, 'sample dataNode xpath', NO_TIMESTAMP);
 
 397     def 'Delete multiple data nodes via xPath'() {
 
 398         when: 'Delete data nodes method is called with multiple xpaths as parameters'
 
 399             objectUnderTest.deleteDataNodes(['xpath1', 'xpath2'])
 
 400         then: 'the cps data service method to delete data nodes is invoked once with the same xPaths'
 
 401             1 * mockCpsDataService.deleteDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, ['xpath1', 'xpath2'], NO_TIMESTAMP);
 
 404     def 'CM handle exists'() {
 
 405         given: 'data service returns a datanode with correct cm handle id'
 
 406             mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
 
 407         expect: 'cm handle exists for given cm handle id'
 
 408             assert true == objectUnderTest.isExistingCmHandleId(cmHandleId)
 
 411     def 'CM handle does not exist, empty dataNode collection returned'() {
 
 412         given: 'data service returns an empty datanode'
 
 413             mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> []
 
 414         expect: 'false is returned for non-existent cm handle'
 
 415             assert false == objectUnderTest.isExistingCmHandleId(cmHandleId)
 
 418     def 'CM handle does not exist, exception thrown'() {
 
 419         given: 'data service throws an exception'
 
 420             mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, "/dmi-registry/cm-handles[@id='non-existent-cm-handle']", INCLUDE_ALL_DESCENDANTS) >> {throw new DataNodeNotFoundException('','')}
 
 421         expect: 'false is returned for non-existent cm handle'
 
 422             assert false == objectUnderTest.isExistingCmHandleId('non-existent-cm-handle')