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 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.ncmp.api.impl.yangmodels.YangModelCmHandle
30 import org.onap.cps.spi.CascadeDeleteAllowed
31 import org.onap.cps.spi.FetchDescendantsOption
32 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
33 import org.onap.cps.spi.model.DataNode
34 import org.onap.cps.spi.model.ModuleDefinition
35 import org.onap.cps.spi.model.ModuleReference
36 import org.onap.cps.spi.utils.CpsValidator
37 import org.onap.cps.utils.JsonObjectMapper
38 import spock.lang.Shared
39 import spock.lang.Specification
41 import java.time.OffsetDateTime
42 import java.time.ZoneOffset
43 import java.time.format.DateTimeFormatter
45 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
46 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
47 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT
48 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
49 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NO_TIMESTAMP
50 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
51 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
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 'Attempt to get non existing cm handle data node by alternate id'() {
307 given: 'query service is invoked and returns empty collection of data nodes'
308 mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> []
309 when: 'getting the cm handle data node'
310 objectUnderTest.getCmHandleDataNodeByAlternateId('alternate id')
311 then: 'no data found exception thrown'
312 def thrownException = thrown(DataNodeNotFoundException)
313 assert thrownException.getMessage().contains('DataNode not found')
316 def 'Get CM handles that has given module names'() {
317 when: 'the method to get cm handles is called'
318 objectUnderTest.getCmHandleIdsWithGivenModules(['sample-module-name'])
319 then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
320 1 * mockCpsAnchorService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['sample-module-name'])
323 def 'Replace list content'() {
324 when: 'replace list content method is called with xpath and data nodes collection'
325 objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
326 then: 'the cps data service method to replace list content is invoked once with same parameters'
327 1 * mockCpsDataService.replaceListContent(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xpath', [new DataNode()], NO_TIMESTAMP);
330 def 'Delete data node via xPath'() {
331 when: 'Delete data node method is called with xpath as parameter'
332 objectUnderTest.deleteDataNode('sample dataNode xpath')
333 then: 'the cps data service method to delete data node is invoked once with the same xPath'
334 1 * mockCpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, 'sample dataNode xpath', NO_TIMESTAMP);
337 def 'Delete multiple data nodes via xPath'() {
338 when: 'Delete data nodes method is called with multiple xpaths as parameters'
339 objectUnderTest.deleteDataNodes(['xpath1', 'xpath2'])
340 then: 'the cps data service method to delete data nodes is invoked once with the same xPaths'
341 1 * mockCpsDataService.deleteDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, ['xpath1', 'xpath2'], NO_TIMESTAMP);