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.spi.exceptions.DataNodeNotFoundException
41 import org.onap.cps.spi.model.DataNode
42 import org.onap.cps.spi.model.ModuleDefinition
43 import org.onap.cps.spi.model.ModuleReference
44 import org.onap.cps.spi.utils.CpsValidator
45 import org.onap.cps.utils.JsonObjectMapper
46 import spock.lang.Shared
47 import spock.lang.Specification
48 import java.time.OffsetDateTime
49 import java.time.ZoneOffset
50 import java.time.format.DateTimeFormatter
52 class InventoryPersistenceImplSpec extends Specification {
54 def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
56 def mockCpsDataService = Mock(CpsDataService)
58 def mockCpsModuleService = Mock(CpsModuleService)
60 def mockCpsAnchorService = Mock(CpsAnchorService)
62 def mockCpsValidator = Mock(CpsValidator)
64 def mockCmHandleQueries = Mock(CmHandleQueries)
66 def objectUnderTest = new InventoryPersistenceImpl(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService,
67 mockCpsValidator, mockCpsAnchorService, mockCmHandleQueries)
69 def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
70 .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
72 def cmHandleId = 'some-cm-handle'
73 def leaves = ["id":cmHandleId,"dmi-service-name":"common service name","dmi-data-service-name":"data service name","dmi-model-service-name":"model service name"]
74 def xpath = "/dmi-registry/cm-handles[@id='some-cm-handle']"
76 def cmHandleId2 = 'another-cm-handle'
77 def xpath2 = "/dmi-registry/cm-handles[@id='another-cm-handle']"
80 def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"]),
81 new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
84 def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])]
87 def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
90 def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
92 def "Retrieve CmHandle using datanode with #scenario."() {
93 given: 'the cps data service returns a data node from the DMI registry'
94 def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
95 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
96 when: 'retrieving the yang modelled cm handle'
97 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
98 then: 'the result has the correct id and service names'
99 result.id == cmHandleId
100 result.dmiServiceName == 'common service name'
101 result.dmiDataServiceName == 'data service name'
102 result.dmiModelServiceName == 'model service name'
103 and: 'the expected DMI properties'
104 result.dmiProperties == expectedDmiProperties
105 result.publicProperties == expectedPublicProperties
106 and: 'the state details are returned'
107 result.compositeState.cmHandleState == expectedCompositeState
108 and: 'the CM Handle ID is validated'
109 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
110 where: 'the following parameters are used'
111 scenario | childDataNodes || expectedDmiProperties || expectedPublicProperties || expectedCompositeState
112 'no properties' | [] || [] || [] || null
113 'DMI and public properties' | childDataNodesForCmHandleWithAllProperties || [new YangModelCmHandle.Property("name1", "value1")] || [new YangModelCmHandle.Property("name2", "value2")] || null
114 'just DMI properties' | childDataNodesForCmHandleWithDMIProperties || [new YangModelCmHandle.Property("name1", "value1")] || [] || null
115 'just public properties' | childDataNodesForCmHandleWithPublicProperties || [] || [new YangModelCmHandle.Property("name2", "value2")] || null
116 'with state details' | childDataNodesForCmHandleWithState || [] || [] || CmHandleState.ADVISED
119 def "Handling missing service names as null."() {
120 given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes'
121 def dataNode = new DataNode(childDataNodes:[], leaves: ['id':cmHandleId])
122 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
123 when: 'retrieving the yang modelled cm handle'
124 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
125 then: 'the service names are returned as null'
126 result.dmiServiceName == null
127 result.dmiDataServiceName == null
128 result.dmiModelServiceName == null
129 and: 'the CM Handle ID is validated'
130 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
133 def "Retrieve multiple YangModelCmHandles"() {
134 given: 'the cps data service returns 2 data nodes from the DMI registry'
135 def dataNodes = [new DataNode(xpath: xpath, leaves: ['id': cmHandleId]), new DataNode(xpath: xpath2, leaves: ['id': cmHandleId2])]
136 mockCpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, [xpath, xpath2] , INCLUDE_ALL_DESCENDANTS) >> dataNodes
137 when: 'retrieving the yang modelled cm handle'
138 def results = objectUnderTest.getYangModelCmHandles([cmHandleId, cmHandleId2])
139 then: 'verify both have returned and cmhandleIds are correct'
140 assert results.size() == 2
141 assert results.id.containsAll([cmHandleId, cmHandleId2])
144 def 'Get a Cm Handle Composite State'() {
145 given: 'a valid cm handle id'
146 def cmHandleId = 'Some-Cm-Handle'
147 def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
148 and: 'cps data service returns a valid data node'
149 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
150 '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode]
151 when: 'get cm handle state is invoked'
152 def result = objectUnderTest.getCmHandleState(cmHandleId)
153 then: 'result has returned the correct cm handle state'
154 result.cmHandleState == CmHandleState.ADVISED
155 and: 'the CM Handle ID is validated'
156 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
159 def 'Update Cm Handle with #scenario State'() {
160 given: 'a cm handle and a composite state'
161 def cmHandleId = 'Some-Cm-Handle'
162 def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
163 when: 'update cm handle state is invoked with the #scenario state'
164 objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
165 then: 'update node leaves is invoked with the correct params'
166 1 * mockCpsDataService.updateDataNodeAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
167 where: 'the following states are used'
168 scenario | cmHandleState || expectedJsonData
169 'READY' | CmHandleState.READY || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
170 'LOCKED' | CmHandleState.LOCKED || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
171 'DELETING' | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
174 def 'Update Cm Handles with #scenario States'() {
175 given: 'a map of cm handles composite states'
176 def compositeState1 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
177 def compositeState2 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
178 when: 'update cm handle state is invoked with the #scenario state'
179 def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2]
180 objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
181 then: 'update node leaves is invoked with the correct params'
182 1 * mockCpsDataService.updateDataNodesAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandlesJsonDataMap, _ as OffsetDateTime)
183 where: 'the following states are used'
184 scenario | cmHandleState || cmHandlesJsonDataMap
185 '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"}}']
186 '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"}}']
187 '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"}}']
190 def 'Getting module definitions by module'() {
191 given: 'cps module service returns module definition for module name'
192 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
193 mockCpsModuleService.getModuleDefinitionsByAnchorAndModule(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id', 'some-module', '2024-01-25') >> moduleDefinitions
194 when: 'get module definitions is invoked with module name'
195 def result = objectUnderTest.getModuleDefinitionsByCmHandleAndModule('some-cmHandle-Id', 'some-module', '2024-01-25')
196 then: 'returned result are the same module definitions as returned from module service'
197 assert result == moduleDefinitions
198 and: 'cm handle id and module name validated'
199 1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id', 'some-module')
202 def 'Getting module definitions with cm handle id'() {
203 given: 'cps module service returns module definitions for cm handle id'
204 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
205 mockCpsModuleService.getModuleDefinitionsByAnchorName(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleDefinitions
206 when: 'get module definitions is invoked with cm handle id'
207 def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
208 then: 'the returned result are the same module definitions as returned from the module service'
209 assert result == moduleDefinitions
212 def 'Get module references'() {
213 given: 'cps module service returns a collection of module references'
214 def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
215 mockCpsModuleService.getYangResourcesModuleReferences(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleReferences
216 when: 'get yang resources module references by cmHandle is invoked'
217 def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
218 then: 'the returned result is a collection of module definitions'
219 assert result == moduleReferences
220 and: 'the CM Handle ID is validated'
221 1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id')
224 def 'Save Cmhandle'() {
225 given: 'cmHandle represented as Yang Model'
226 def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: [])
227 when: 'the method to save cmhandle is called'
228 objectUnderTest.saveCmHandle(yangModelCmHandle)
229 then: 'the data service method to save list elements is called once'
230 1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT,
233 assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
238 def 'Save Multiple Cmhandles'() {
239 given: 'cm handles represented as Yang Model'
240 def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1')
241 def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2')
242 when: 'the cm handles are saved'
243 objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2])
244 then: 'CPS Data Service persists both cm handles as a batch'
245 1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
246 NCMP_DMI_REGISTRY_PARENT, _,null) >> {
248 def jsonData = (args[3] as String)
249 jsonData.contains('cmhandle1')
250 jsonData.contains('cmhandle2')
255 def 'Delete list or list elements'() {
256 when: 'the method to delete list or list elements is called'
257 objectUnderTest.deleteListOrListElement('sample xPath')
258 then: 'the data service method to save list elements is called once'
259 1 * mockCpsDataService.deleteListOrListElement(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath',null)
262 def 'Delete schema set with a valid schema set name'() {
263 when: 'the method to delete schema set is called with valid schema set name'
264 objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
265 then: 'the module service to delete schemaSet is invoked once'
266 1 * mockCpsModuleService.deleteSchemaSet(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
267 and: 'the schema set name is validated'
268 1 * mockCpsValidator.validateNameCharacters('validSchemaSetName')
271 def 'Delete multiple schema sets with valid schema set names'() {
272 when: 'the method to delete schema sets is called with valid schema set names'
273 objectUnderTest.deleteSchemaSetsWithCascade(['validSchemaSetName1', 'validSchemaSetName2'])
274 then: 'the module service to delete schema sets is invoked once'
275 1 * mockCpsModuleService.deleteSchemaSetsWithCascade(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['validSchemaSetName1', 'validSchemaSetName2'])
276 and: 'the schema set names are validated'
277 1 * mockCpsValidator.validateNameCharacters(['validSchemaSetName1', 'validSchemaSetName2'])
280 def 'Get data node via xPath'() {
281 when: 'the method to get data nodes is called'
282 objectUnderTest.getDataNode('sample xPath')
283 then: 'the data persistence service method to get data node is invoked once'
284 1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath', INCLUDE_ALL_DESCENDANTS)
287 def 'Get cmHandle data node'() {
288 given: 'expected xPath to get cmHandle data node'
289 def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']'
290 when: 'the method to get data nodes is called'
291 objectUnderTest.getCmHandleDataNodeByCmHandleId('sample cmHandleId')
292 then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
293 1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, expectedXPath, INCLUDE_ALL_DESCENDANTS)
296 def 'Get cm handle data node'() {
297 given: 'expected xPath to get cmHandle data node'
298 def expectedXPath = '/dmi-registry/cm-handles[@alternate-id=\'alternate id\']'
299 and: 'query service is invoked with expected xpath'
300 mockCmHandleQueries.queryNcmpRegistryByCpsPath(expectedXPath, OMIT_DESCENDANTS) >> [new DataNode()]
301 expect: 'getting the cm handle data node'
302 assert objectUnderTest.getCmHandleDataNodeByAlternateId('alternate id') == new DataNode()
305 def 'Attempt to get non existing cm handle data node by alternate id'() {
306 given: 'query service is invoked and returns empty collection of data nodes'
307 mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> []
308 when: 'getting the cm handle data node'
309 objectUnderTest.getCmHandleDataNodeByAlternateId('alternate id')
310 then: 'no data found exception thrown'
311 def thrownException = thrown(DataNodeNotFoundException)
312 assert thrownException.getMessage().contains('DataNode not found')
315 def 'Get CM handles that has given module names'() {
316 when: 'the method to get cm handles is called'
317 objectUnderTest.getCmHandleIdsWithGivenModules(['sample-module-name'])
318 then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
319 1 * mockCpsAnchorService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['sample-module-name'])
322 def 'Replace list content'() {
323 when: 'replace list content method is called with xpath and data nodes collection'
324 objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
325 then: 'the cps data service method to replace list content is invoked once with same parameters'
326 1 * mockCpsDataService.replaceListContent(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xpath', [new DataNode()], NO_TIMESTAMP);
329 def 'Delete data node via xPath'() {
330 when: 'Delete data node method is called with xpath as parameter'
331 objectUnderTest.deleteDataNode('sample dataNode xpath')
332 then: 'the cps data service method to delete data node is invoked once with the same xPath'
333 1 * mockCpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, 'sample dataNode xpath', NO_TIMESTAMP);
336 def 'Delete multiple data nodes via xPath'() {
337 when: 'Delete data nodes method is called with multiple xpaths as parameters'
338 objectUnderTest.deleteDataNodes(['xpath1', 'xpath2'])
339 then: 'the cps data service method to delete data nodes is invoked once with the same xPaths'
340 1 * mockCpsDataService.deleteDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, ['xpath1', 'xpath2'], NO_TIMESTAMP);