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.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.inventory.models.CompositeState
30 import org.onap.cps.ncmp.impl.inventory.models.CmHandleState
31 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
32 import org.onap.cps.spi.CascadeDeleteAllowed
33 import org.onap.cps.spi.FetchDescendantsOption
34 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
35 import org.onap.cps.spi.model.DataNode
36 import org.onap.cps.spi.model.ModuleDefinition
37 import org.onap.cps.spi.model.ModuleReference
38 import org.onap.cps.spi.utils.CpsValidator
39 import org.onap.cps.utils.JsonObjectMapper
40 import spock.lang.Shared
41 import spock.lang.Specification
43 import java.time.OffsetDateTime
44 import java.time.ZoneOffset
45 import java.time.format.DateTimeFormatter
47 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DATASPACE_NAME
48 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
49 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT
50 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
51 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NO_TIMESTAMP
52 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
53 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
55 class InventoryPersistenceImplSpec extends Specification {
57 def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
59 def mockCpsDataService = Mock(CpsDataService)
61 def mockCpsModuleService = Mock(CpsModuleService)
63 def mockCpsAnchorService = Mock(CpsAnchorService)
65 def mockCpsValidator = Mock(CpsValidator)
67 def mockCmHandleQueries = Mock(CmHandleQueryService)
69 def objectUnderTest = new InventoryPersistenceImpl(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService,
70 mockCpsValidator, mockCpsAnchorService, mockCmHandleQueries)
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))
75 def cmHandleId = 'some-cm-handle'
76 def leaves = ["id":cmHandleId,"dmi-service-name":"common service name","dmi-data-service-name":"data service name","dmi-model-service-name":"model service name"]
77 def xpath = "/dmi-registry/cm-handles[@id='some-cm-handle']"
79 def cmHandleId2 = 'another-cm-handle'
80 def xpath2 = "/dmi-registry/cm-handles[@id='another-cm-handle']"
83 def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"]),
84 new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
87 def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])]
90 def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
93 def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
95 def "Retrieve CmHandle using datanode with #scenario."() {
96 given: 'the cps data service returns a data node from the DMI registry'
97 def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
98 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
99 when: 'retrieving the yang modelled cm handle'
100 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
101 then: 'the result has the correct id and service names'
102 result.id == cmHandleId
103 result.dmiServiceName == 'common service name'
104 result.dmiDataServiceName == 'data service name'
105 result.dmiModelServiceName == 'model service name'
106 and: 'the expected DMI properties'
107 result.dmiProperties == expectedDmiProperties
108 result.publicProperties == expectedPublicProperties
109 and: 'the state details are returned'
110 result.compositeState.cmHandleState == expectedCompositeState
111 and: 'the CM Handle ID is validated'
112 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
113 where: 'the following parameters are used'
114 scenario | childDataNodes || expectedDmiProperties || expectedPublicProperties || expectedCompositeState
115 'no properties' | [] || [] || [] || null
116 'DMI and public properties' | childDataNodesForCmHandleWithAllProperties || [new YangModelCmHandle.Property("name1", "value1")] || [new YangModelCmHandle.Property("name2", "value2")] || null
117 'just DMI properties' | childDataNodesForCmHandleWithDMIProperties || [new YangModelCmHandle.Property("name1", "value1")] || [] || null
118 'just public properties' | childDataNodesForCmHandleWithPublicProperties || [] || [new YangModelCmHandle.Property("name2", "value2")] || null
119 'with state details' | childDataNodesForCmHandleWithState || [] || [] || CmHandleState.ADVISED
122 def "Handling missing service names as null."() {
123 given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes'
124 def dataNode = new DataNode(childDataNodes:[], leaves: ['id':cmHandleId])
125 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
126 when: 'retrieving the yang modelled cm handle'
127 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
128 then: 'the service names are returned as null'
129 result.dmiServiceName == null
130 result.dmiDataServiceName == null
131 result.dmiModelServiceName == null
132 and: 'the CM Handle ID is validated'
133 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
136 def "Retrieve multiple YangModelCmHandles"() {
137 given: 'the cps data service returns 2 data nodes from the DMI registry'
138 def dataNodes = [new DataNode(xpath: xpath, leaves: ['id': cmHandleId]), new DataNode(xpath: xpath2, leaves: ['id': cmHandleId2])]
139 mockCpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, [xpath, xpath2] , INCLUDE_ALL_DESCENDANTS) >> dataNodes
140 when: 'retrieving the yang modelled cm handle'
141 def results = objectUnderTest.getYangModelCmHandles([cmHandleId, cmHandleId2])
142 then: 'verify both have returned and cmhandleIds are correct'
143 assert results.size() == 2
144 assert results.id.containsAll([cmHandleId, cmHandleId2])
147 def 'Get a Cm Handle Composite State'() {
148 given: 'a valid cm handle id'
149 def cmHandleId = 'Some-Cm-Handle'
150 def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
151 and: 'cps data service returns a valid data node'
152 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
153 '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode]
154 when: 'get cm handle state is invoked'
155 def result = objectUnderTest.getCmHandleState(cmHandleId)
156 then: 'result has returned the correct cm handle state'
157 result.cmHandleState == CmHandleState.ADVISED
158 and: 'the CM Handle ID is validated'
159 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
162 def 'Update Cm Handle with #scenario State'() {
163 given: 'a cm handle and a composite state'
164 def cmHandleId = 'Some-Cm-Handle'
165 def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
166 when: 'update cm handle state is invoked with the #scenario state'
167 objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
168 then: 'update node leaves is invoked with the correct params'
169 1 * mockCpsDataService.updateDataNodeAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
170 where: 'the following states are used'
171 scenario | cmHandleState || expectedJsonData
172 'READY' | CmHandleState.READY || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
173 'LOCKED' | CmHandleState.LOCKED || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
174 'DELETING' | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
177 def 'Update Cm Handles with #scenario States'() {
178 given: 'a map of cm handles composite states'
179 def compositeState1 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
180 def compositeState2 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
181 when: 'update cm handle state is invoked with the #scenario state'
182 def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2]
183 objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
184 then: 'update node leaves is invoked with the correct params'
185 1 * mockCpsDataService.updateDataNodesAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandlesJsonDataMap, _ as OffsetDateTime)
186 where: 'the following states are used'
187 scenario | cmHandleState || cmHandlesJsonDataMap
188 '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"}}']
189 '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"}}']
190 '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"}}']
193 def 'Getting module definitions by module'() {
194 given: 'cps module service returns module definition for module name'
195 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
196 mockCpsModuleService.getModuleDefinitionsByAnchorAndModule(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id', 'some-module', '2024-01-25') >> moduleDefinitions
197 when: 'get module definitions is invoked with module name'
198 def result = objectUnderTest.getModuleDefinitionsByCmHandleAndModule('some-cmHandle-Id', 'some-module', '2024-01-25')
199 then: 'returned result are the same module definitions as returned from module service'
200 assert result == moduleDefinitions
201 and: 'cm handle id and module name validated'
202 1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id', 'some-module')
205 def 'Getting module definitions with cm handle id'() {
206 given: 'cps module service returns module definitions for cm handle id'
207 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
208 mockCpsModuleService.getModuleDefinitionsByAnchorName(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleDefinitions
209 when: 'get module definitions is invoked with cm handle id'
210 def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
211 then: 'the returned result are the same module definitions as returned from the module service'
212 assert result == moduleDefinitions
215 def 'Get module references'() {
216 given: 'cps module service returns a collection of module references'
217 def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
218 mockCpsModuleService.getYangResourcesModuleReferences(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleReferences
219 when: 'get yang resources module references by cmHandle is invoked'
220 def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
221 then: 'the returned result is a collection of module definitions'
222 assert result == moduleReferences
223 and: 'the CM Handle ID is validated'
224 1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id')
227 def 'Save Cmhandle'() {
228 given: 'cmHandle represented as Yang Model'
229 def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: [])
230 when: 'the method to save cmhandle is called'
231 objectUnderTest.saveCmHandle(yangModelCmHandle)
232 then: 'the data service method to save list elements is called once'
233 1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT,
236 assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
241 def 'Save Multiple Cmhandles'() {
242 given: 'cm handles represented as Yang Model'
243 def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1')
244 def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2')
245 when: 'the cm handles are saved'
246 objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2])
247 then: 'CPS Data Service persists both cm handles as a batch'
248 1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
249 NCMP_DMI_REGISTRY_PARENT, _,null) >> {
251 def jsonData = (args[3] as String)
252 jsonData.contains('cmhandle1')
253 jsonData.contains('cmhandle2')
258 def 'Delete list or list elements'() {
259 when: 'the method to delete list or list elements is called'
260 objectUnderTest.deleteListOrListElement('sample xPath')
261 then: 'the data service method to save list elements is called once'
262 1 * mockCpsDataService.deleteListOrListElement(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath',null)
265 def 'Delete schema set with a valid schema set name'() {
266 when: 'the method to delete schema set is called with valid schema set name'
267 objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
268 then: 'the module service to delete schemaSet is invoked once'
269 1 * mockCpsModuleService.deleteSchemaSet(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
270 and: 'the schema set name is validated'
271 1 * mockCpsValidator.validateNameCharacters('validSchemaSetName')
274 def 'Delete multiple schema sets with valid schema set names'() {
275 when: 'the method to delete schema sets is called with valid schema set names'
276 objectUnderTest.deleteSchemaSetsWithCascade(['validSchemaSetName1', 'validSchemaSetName2'])
277 then: 'the module service to delete schema sets is invoked once'
278 1 * mockCpsModuleService.deleteSchemaSetsWithCascade(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['validSchemaSetName1', 'validSchemaSetName2'])
279 and: 'the schema set names are validated'
280 1 * mockCpsValidator.validateNameCharacters(['validSchemaSetName1', 'validSchemaSetName2'])
283 def 'Get data node via xPath'() {
284 when: 'the method to get data nodes is called'
285 objectUnderTest.getDataNode('sample xPath')
286 then: 'the data persistence service method to get data node is invoked once'
287 1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath', INCLUDE_ALL_DESCENDANTS)
290 def 'Get cmHandle data node'() {
291 given: 'expected xPath to get cmHandle data node'
292 def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']'
293 when: 'the method to get data nodes is called'
294 objectUnderTest.getCmHandleDataNodeByCmHandleId('sample cmHandleId')
295 then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
296 1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, expectedXPath, INCLUDE_ALL_DESCENDANTS)
299 def 'Get cm handle data node'() {
300 given: 'expected xPath to get cmHandle data node'
301 def expectedXPath = '/dmi-registry/cm-handles[@alternate-id=\'alternate id\']'
302 and: 'query service is invoked with expected xpath'
303 mockCmHandleQueries.queryNcmpRegistryByCpsPath(expectedXPath, OMIT_DESCENDANTS) >> [new DataNode()]
304 expect: 'getting the cm handle data node'
305 assert objectUnderTest.getCmHandleDataNodeByAlternateId('alternate id') == new DataNode()
308 def 'Attempt to get non existing cm handle data node by alternate id'() {
309 given: 'query service is invoked and returns empty collection of data nodes'
310 mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> []
311 when: 'getting the cm handle data node'
312 objectUnderTest.getCmHandleDataNodeByAlternateId('alternate id')
313 then: 'no data found exception thrown'
314 def thrownException = thrown(DataNodeNotFoundException)
315 assert thrownException.getMessage().contains('DataNode not found')
318 def 'Get CM handles that has given module names'() {
319 when: 'the method to get cm handles is called'
320 objectUnderTest.getCmHandleIdsWithGivenModules(['sample-module-name'])
321 then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
322 1 * mockCpsAnchorService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['sample-module-name'])
325 def 'Replace list content'() {
326 when: 'replace list content method is called with xpath and data nodes collection'
327 objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
328 then: 'the cps data service method to replace list content is invoked once with same parameters'
329 1 * mockCpsDataService.replaceListContent(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xpath', [new DataNode()], NO_TIMESTAMP);
332 def 'Delete data node via xPath'() {
333 when: 'Delete data node method is called with xpath as parameter'
334 objectUnderTest.deleteDataNode('sample dataNode xpath')
335 then: 'the cps data service method to delete data node is invoked once with the same xPath'
336 1 * mockCpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, 'sample dataNode xpath', NO_TIMESTAMP);
339 def 'Delete multiple data nodes via xPath'() {
340 when: 'Delete data nodes method is called with multiple xpaths as parameters'
341 objectUnderTest.deleteDataNodes(['xpath1', 'xpath2'])
342 then: 'the cps data service method to delete data nodes is invoked once with the same xPaths'
343 1 * mockCpsDataService.deleteDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, ['xpath1', 'xpath2'], NO_TIMESTAMP);