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