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 org.onap.cps.api.CpsAnchorService
27 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
28 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
29 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
30 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT
31 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NO_TIMESTAMP
32 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
34 import com.fasterxml.jackson.databind.ObjectMapper
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.model.DataNode
41 import org.onap.cps.spi.model.ModuleDefinition
42 import org.onap.cps.spi.model.ModuleReference
43 import org.onap.cps.utils.JsonObjectMapper
44 import org.onap.cps.spi.utils.CpsValidator
45 import spock.lang.Shared
46 import spock.lang.Specification
47 import java.time.OffsetDateTime
48 import java.time.ZoneOffset
49 import java.time.format.DateTimeFormatter
51 class InventoryPersistenceImplSpec extends Specification {
53 def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
55 def mockCpsDataService = Mock(CpsDataService)
57 def mockCpsModuleService = Mock(CpsModuleService)
59 def mockCpsAnchorService = Mock(CpsAnchorService)
61 def mockCpsValidator = Mock(CpsValidator)
63 def objectUnderTest = new InventoryPersistenceImpl(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService,
64 mockCpsValidator, mockCpsAnchorService)
66 def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
67 .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
69 def cmHandleId = 'some-cm-handle'
70 def leaves = ["dmi-service-name":"common service name","dmi-data-service-name":"data service name","dmi-model-service-name":"model service name"]
71 def xpath = "/dmi-registry/cm-handles[@id='some-cm-handle']"
73 def cmHandleId2 = 'another-cm-handle'
74 def xpath2 = "/dmi-registry/cm-handles[@id='another-cm-handle']"
77 def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"]),
78 new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
81 def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])]
84 def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
87 def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
89 def "Retrieve CmHandle using datanode with #scenario."() {
90 given: 'the cps data service returns a data node from the DMI registry'
91 def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
92 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
93 when: 'retrieving the yang modelled cm handle'
94 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
95 then: 'the result has the correct id and service names'
96 result.id == cmHandleId
97 result.dmiServiceName == 'common service name'
98 result.dmiDataServiceName == 'data service name'
99 result.dmiModelServiceName == 'model service name'
100 and: 'the expected DMI properties'
101 result.dmiProperties == expectedDmiProperties
102 result.publicProperties == expectedPublicProperties
103 and: 'the state details are returned'
104 result.compositeState.cmHandleState == expectedCompositeState
105 and: 'the CM Handle ID is validated'
106 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
107 where: 'the following parameters are used'
108 scenario | childDataNodes || expectedDmiProperties || expectedPublicProperties || expectedCompositeState
109 'no properties' | [] || [] || [] || null
110 'DMI and public properties' | childDataNodesForCmHandleWithAllProperties || [new YangModelCmHandle.Property("name1", "value1")] || [new YangModelCmHandle.Property("name2", "value2")] || null
111 'just DMI properties' | childDataNodesForCmHandleWithDMIProperties || [new YangModelCmHandle.Property("name1", "value1")] || [] || null
112 'just public properties' | childDataNodesForCmHandleWithPublicProperties || [] || [new YangModelCmHandle.Property("name2", "value2")] || null
113 'with state details' | childDataNodesForCmHandleWithState || [] || [] || CmHandleState.ADVISED
116 def "Handling missing service names as null."() {
117 given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes'
118 def dataNode = new DataNode(childDataNodes:[], leaves: [:])
119 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
120 when: 'retrieving the yang modelled cm handle'
121 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
122 then: 'the service names are returned as null'
123 result.dmiServiceName == null
124 result.dmiDataServiceName == null
125 result.dmiModelServiceName == null
126 and: 'the CM Handle ID is validated'
127 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
130 def "Retrieve multiple YangModelCmHandles"() {
131 given: 'the cps data service returns 2 data nodes from the DMI registry'
132 def dataNodes = [new DataNode(xpath: xpath), new DataNode(xpath: xpath2)]
133 mockCpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, [xpath, xpath2] , INCLUDE_ALL_DESCENDANTS) >> dataNodes
134 when: 'retrieving the yang modelled cm handle'
135 def results = objectUnderTest.getYangModelCmHandles([cmHandleId, cmHandleId2])
136 then: 'verify both have returned and cmhandleIds are correct'
137 assert results.size() == 2
138 assert results.id.containsAll([cmHandleId, cmHandleId2])
141 def 'Get a Cm Handle Composite State'() {
142 given: 'a valid cm handle id'
143 def cmHandleId = 'Some-Cm-Handle'
144 def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
145 and: 'cps data service returns a valid data node'
146 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
147 '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode]
148 when: 'get cm handle state is invoked'
149 def result = objectUnderTest.getCmHandleState(cmHandleId)
150 then: 'result has returned the correct cm handle state'
151 result.cmHandleState == CmHandleState.ADVISED
152 and: 'the CM Handle ID is validated'
153 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
156 def 'Update Cm Handle with #scenario State'() {
157 given: 'a cm handle and a composite state'
158 def cmHandleId = 'Some-Cm-Handle'
159 def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
160 when: 'update cm handle state is invoked with the #scenario state'
161 objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
162 then: 'update node leaves is invoked with the correct params'
163 1 * mockCpsDataService.updateDataNodeAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
164 where: 'the following states are used'
165 scenario | cmHandleState || expectedJsonData
166 'READY' | CmHandleState.READY || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
167 'LOCKED' | CmHandleState.LOCKED || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
168 'DELETING' | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
171 def 'Update Cm Handles with #scenario States'() {
172 given: 'a map of cm handles composite states'
173 def compositeState1 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
174 def compositeState2 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
175 when: 'update cm handle state is invoked with the #scenario state'
176 def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2]
177 objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
178 then: 'update node leaves is invoked with the correct params'
179 1 * mockCpsDataService.updateDataNodesAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandlesJsonDataMap, _ as OffsetDateTime)
180 where: 'the following states are used'
181 scenario | cmHandleState || cmHandlesJsonDataMap
182 '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"}}']
183 '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"}}']
184 '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"}}']
187 def 'Getting module definitions by module'() {
188 given: 'cps module service returns module definition for module name'
189 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
190 mockCpsModuleService.getModuleDefinitionsByAnchorAndModule(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id', 'some-module', '2024-01-25') >> moduleDefinitions
191 when: 'get module definitions is invoked with module name'
192 def result = objectUnderTest.getModuleDefinitionsByCmHandleAndModule('some-cmHandle-Id', 'some-module', '2024-01-25')
193 then: 'returned result are the same module definitions as returned from module service'
194 assert result == moduleDefinitions
195 and: 'cm handle id and module name validated'
196 1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id', 'some-module')
199 def 'Getting module definitions with cm handle id'() {
200 given: 'cps module service returns module definitions for cm handle id'
201 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
202 mockCpsModuleService.getModuleDefinitionsByAnchorName(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleDefinitions
203 when: 'get module definitions is invoked with cm handle id'
204 def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
205 then: 'the returned result are the same module definitions as returned from the module service'
206 assert result == moduleDefinitions
209 def 'Get module references'() {
210 given: 'cps module service returns a collection of module references'
211 def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
212 mockCpsModuleService.getYangResourcesModuleReferences(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleReferences
213 when: 'get yang resources module references by cmHandle is invoked'
214 def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
215 then: 'the returned result is a collection of module definitions'
216 assert result == moduleReferences
217 and: 'the CM Handle ID is validated'
218 1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id')
221 def 'Save Cmhandle'() {
222 given: 'cmHandle represented as Yang Model'
223 def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: [])
224 when: 'the method to save cmhandle is called'
225 objectUnderTest.saveCmHandle(yangModelCmHandle)
226 then: 'the data service method to save list elements is called once'
227 1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT,
230 assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
235 def 'Save Multiple Cmhandles'() {
236 given: 'cm handles represented as Yang Model'
237 def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1')
238 def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2')
239 when: 'the cm handles are saved'
240 objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2])
241 then: 'CPS Data Service persists both cm handles as a batch'
242 1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
243 NCMP_DMI_REGISTRY_PARENT, _,null) >> {
245 def jsonData = (args[3] as String)
246 jsonData.contains('cmhandle1')
247 jsonData.contains('cmhandle2')
252 def 'Delete list or list elements'() {
253 when: 'the method to delete list or list elements is called'
254 objectUnderTest.deleteListOrListElement('sample xPath')
255 then: 'the data service method to save list elements is called once'
256 1 * mockCpsDataService.deleteListOrListElement(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath',null)
259 def 'Delete schema set with a valid schema set name'() {
260 when: 'the method to delete schema set is called with valid schema set name'
261 objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
262 then: 'the module service to delete schemaSet is invoked once'
263 1 * mockCpsModuleService.deleteSchemaSet(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
264 and: 'the schema set name is validated'
265 1 * mockCpsValidator.validateNameCharacters('validSchemaSetName')
268 def 'Delete multiple schema sets with valid schema set names'() {
269 when: 'the method to delete schema sets is called with valid schema set names'
270 objectUnderTest.deleteSchemaSetsWithCascade(['validSchemaSetName1', 'validSchemaSetName2'])
271 then: 'the module service to delete schema sets is invoked once'
272 1 * mockCpsModuleService.deleteSchemaSetsWithCascade(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['validSchemaSetName1', 'validSchemaSetName2'])
273 and: 'the schema set names are validated'
274 1 * mockCpsValidator.validateNameCharacters(['validSchemaSetName1', 'validSchemaSetName2'])
277 def 'Get data node via xPath'() {
278 when: 'the method to get data nodes is called'
279 objectUnderTest.getDataNode('sample xPath')
280 then: 'the data persistence service method to get data node is invoked once'
281 1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath', INCLUDE_ALL_DESCENDANTS)
284 def 'Get cmHandle data node'() {
285 given: 'expected xPath to get cmHandle data node'
286 def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']';
287 when: 'the method to get data nodes is called'
288 objectUnderTest.getCmHandleDataNode('sample cmHandleId')
289 then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
290 1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, expectedXPath, INCLUDE_ALL_DESCENDANTS)
293 def 'Get CM handles that has given module names'() {
294 when: 'the method to get cm handles is called'
295 objectUnderTest.getCmHandleIdsWithGivenModules(['sample-module-name'])
296 then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
297 1 * mockCpsAnchorService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['sample-module-name'])
300 def 'Replace list content'() {
301 when: 'replace list content method is called with xpath and data nodes collection'
302 objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
303 then: 'the cps data service method to replace list content is invoked once with same parameters'
304 1 * mockCpsDataService.replaceListContent(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xpath', [new DataNode()], NO_TIMESTAMP);
307 def 'Delete data node via xPath'() {
308 when: 'Delete data node method is called with xpath as parameter'
309 objectUnderTest.deleteDataNode('sample dataNode xpath')
310 then: 'the cps data service method to delete data node is invoked once with the same xPath'
311 1 * mockCpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, 'sample dataNode xpath', NO_TIMESTAMP);
314 def 'Delete multiple data nodes via xPath'() {
315 when: 'Delete data nodes method is called with multiple xpaths as parameters'
316 objectUnderTest.deleteDataNodes(['xpath1', 'xpath2'])
317 then: 'the cps data service method to delete data nodes is invoked once with the same xPaths'
318 1 * mockCpsDataService.deleteDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, ['xpath1', 'xpath2'], NO_TIMESTAMP);