2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2022-2023 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.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
26 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
27 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
28 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT
29 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NO_TIMESTAMP
30 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
32 import com.fasterxml.jackson.databind.ObjectMapper
33 import org.onap.cps.api.CpsAdminService
34 import org.onap.cps.api.CpsDataService
35 import org.onap.cps.api.CpsModuleService
36 import org.onap.cps.ncmp.api.impl.inventory.CmHandleState
37 import org.onap.cps.ncmp.api.impl.inventory.CompositeState
38 import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistenceImpl
39 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
40 import org.onap.cps.spi.CascadeDeleteAllowed
41 import org.onap.cps.spi.FetchDescendantsOption
42 import org.onap.cps.spi.model.DataNode
43 import org.onap.cps.spi.model.ModuleDefinition
44 import org.onap.cps.spi.model.ModuleReference
45 import org.onap.cps.utils.JsonObjectMapper
46 import org.onap.cps.spi.utils.CpsValidator
47 import spock.lang.Shared
48 import spock.lang.Specification
49 import java.time.OffsetDateTime
50 import java.time.ZoneOffset
51 import java.time.format.DateTimeFormatter
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 mockCpsAdminService = Mock(CpsAdminService)
63 def mockCpsValidator = Mock(CpsValidator)
65 def objectUnderTest = new InventoryPersistenceImpl(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService,
66 mockCpsValidator, mockCpsAdminService)
68 def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
69 .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
71 def cmHandleId = 'some-cm-handle'
72 def leaves = ["dmi-service-name":"common service name","dmi-data-service-name":"data service name","dmi-model-service-name":"model service name"]
73 def xpath = "/dmi-registry/cm-handles[@id='some-cm-handle']"
75 def cmHandleId2 = 'another-cm-handle'
76 def xpath2 = "/dmi-registry/cm-handles[@id='another-cm-handle']"
79 def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"]),
80 new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
83 def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])]
86 def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
89 def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
91 def "Retrieve CmHandle using datanode with #scenario."() {
92 given: 'the cps data service returns a data node from the DMI registry'
93 def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
94 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
95 when: 'retrieving the yang modelled cm handle'
96 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
97 then: 'the result has the correct id and service names'
98 result.id == cmHandleId
99 result.dmiServiceName == 'common service name'
100 result.dmiDataServiceName == 'data service name'
101 result.dmiModelServiceName == 'model service name'
102 and: 'the expected DMI properties'
103 result.dmiProperties == expectedDmiProperties
104 result.publicProperties == expectedPublicProperties
105 and: 'the state details are returned'
106 result.compositeState.cmHandleState == expectedCompositeState
107 and: 'the CM Handle ID is validated'
108 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
109 where: 'the following parameters are used'
110 scenario | childDataNodes || expectedDmiProperties || expectedPublicProperties || expectedCompositeState
111 'no properties' | [] || [] || [] || null
112 'DMI and public properties' | childDataNodesForCmHandleWithAllProperties || [new YangModelCmHandle.Property("name1", "value1")] || [new YangModelCmHandle.Property("name2", "value2")] || null
113 'just DMI properties' | childDataNodesForCmHandleWithDMIProperties || [new YangModelCmHandle.Property("name1", "value1")] || [] || null
114 'just public properties' | childDataNodesForCmHandleWithPublicProperties || [] || [new YangModelCmHandle.Property("name2", "value2")] || null
115 'with state details' | childDataNodesForCmHandleWithState || [] || [] || CmHandleState.ADVISED
118 def "Handling missing service names as null."() {
119 given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes'
120 def dataNode = new DataNode(childDataNodes:[], leaves: [:])
121 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
122 when: 'retrieving the yang modelled cm handle'
123 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
124 then: 'the service names are returned as null'
125 result.dmiServiceName == null
126 result.dmiDataServiceName == null
127 result.dmiModelServiceName == null
128 and: 'the CM Handle ID is validated'
129 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
132 def "Retrieve multiple YangModelCmHandles"() {
133 given: 'the cps data service returns 2 data nodes from the DMI registry'
134 def dataNodes = [new DataNode(xpath: xpath), new DataNode(xpath: xpath2)]
135 mockCpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, [xpath, xpath2] , INCLUDE_ALL_DESCENDANTS) >> dataNodes
136 when: 'retrieving the yang modelled cm handle'
137 def results = objectUnderTest.getYangModelCmHandles([cmHandleId, cmHandleId2])
138 then: 'verify both have returned and cmhandleIds are correct'
139 assert results.size() == 2
140 assert results.id.containsAll([cmHandleId, cmHandleId2])
143 def 'Get a Cm Handle Composite State'() {
144 given: 'a valid cm handle id'
145 def cmHandleId = 'Some-Cm-Handle'
146 def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
147 and: 'cps data service returns a valid data node'
148 mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
149 '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode]
150 when: 'get cm handle state is invoked'
151 def result = objectUnderTest.getCmHandleState(cmHandleId)
152 then: 'result has returned the correct cm handle state'
153 result.cmHandleState == CmHandleState.ADVISED
154 and: 'the CM Handle ID is validated'
155 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
158 def 'Update Cm Handle with #scenario State'() {
159 given: 'a cm handle and a composite state'
160 def cmHandleId = 'Some-Cm-Handle'
161 def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
162 when: 'update cm handle state is invoked with the #scenario state'
163 objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
164 then: 'update node leaves is invoked with the correct params'
165 1 * mockCpsDataService.updateDataNodeAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
166 where: 'the following states are used'
167 scenario | cmHandleState || expectedJsonData
168 'READY' | CmHandleState.READY || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
169 'LOCKED' | CmHandleState.LOCKED || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
170 'DELETING' | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
173 def 'Update Cm Handles with #scenario States'() {
174 given: 'a map of cm handles composite states'
175 def compositeState1 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
176 def compositeState2 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
177 when: 'update cm handle state is invoked with the #scenario state'
178 def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2]
179 objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
180 then: 'update node leaves is invoked with the correct params'
181 1 * mockCpsDataService.updateDataNodesAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandlesJsonDataMap, _ as OffsetDateTime)
182 where: 'the following states are used'
183 scenario | cmHandleState || cmHandlesJsonDataMap
184 '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"}}']
185 '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"}}']
186 '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"}}']
189 def 'Get module definitions'() {
190 given: 'cps module service returns a collection of module definitions'
191 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
192 mockCpsModuleService.getModuleDefinitionsByAnchorName(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleDefinitions
193 when: 'get module definitions by cmHandle is invoked'
194 def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
195 then: 'the returned result are the same module definitions as returned from the module service'
196 assert result == moduleDefinitions
199 def 'Get module references'() {
200 given: 'cps module service returns a collection of module references'
201 def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
202 mockCpsModuleService.getYangResourcesModuleReferences(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleReferences
203 when: 'get yang resources module references by cmHandle is invoked'
204 def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
205 then: 'the returned result is a collection of module definitions'
206 assert result == moduleReferences
207 and: 'the CM Handle ID is validated'
208 1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id')
211 def 'Save Cmhandle'() {
212 given: 'cmHandle represented as Yang Model'
213 def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: [])
214 when: 'the method to save cmhandle is called'
215 objectUnderTest.saveCmHandle(yangModelCmHandle)
216 then: 'the data service method to save list elements is called once'
217 1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT,
220 assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
225 def 'Save Multiple Cmhandles'() {
226 given: 'cm handles represented as Yang Model'
227 def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1')
228 def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2')
229 when: 'the cm handles are saved'
230 objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2])
231 then: 'CPS Data Service persists both cm handles as a batch'
232 1 * mockCpsDataService.saveListElementsBatch(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
233 NCMP_DMI_REGISTRY_PARENT, _,null) >> {
235 def jsonDataList = (args[3] as List)
236 (jsonDataList[0] as String).contains('cmhandle1')
237 (jsonDataList[0] as String).contains('cmhandle2')
242 def 'Delete list or list elements'() {
243 when: 'the method to delete list or list elements is called'
244 objectUnderTest.deleteListOrListElement('sample xPath')
245 then: 'the data service method to save list elements is called once'
246 1 * mockCpsDataService.deleteListOrListElement(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath',null)
249 def 'Delete schema set with a valid schema set name'() {
250 when: 'the method to delete schema set is called with valid schema set name'
251 objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
252 then: 'the module service to delete schemaSet is invoked once'
253 1 * mockCpsModuleService.deleteSchemaSet(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
254 and: 'the schema set name is validated'
255 1 * mockCpsValidator.validateNameCharacters('validSchemaSetName')
258 def 'Delete multiple schema sets with valid schema set names'() {
259 when: 'the method to delete schema sets is called with valid schema set names'
260 objectUnderTest.deleteSchemaSetsWithCascade(['validSchemaSetName1', 'validSchemaSetName2'])
261 then: 'the module service to delete schema sets is invoked once'
262 1 * mockCpsModuleService.deleteSchemaSetsWithCascade(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['validSchemaSetName1', 'validSchemaSetName2'])
263 and: 'the schema set names are validated'
264 1 * mockCpsValidator.validateNameCharacters(['validSchemaSetName1', 'validSchemaSetName2'])
267 def 'Get data node via xPath'() {
268 when: 'the method to get data nodes is called'
269 objectUnderTest.getDataNode('sample xPath')
270 then: 'the data persistence service method to get data node is invoked once'
271 1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath', INCLUDE_ALL_DESCENDANTS)
274 def 'Get cmHandle data node'() {
275 given: 'expected xPath to get cmHandle data node'
276 def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']';
277 when: 'the method to get data nodes is called'
278 objectUnderTest.getCmHandleDataNode('sample cmHandleId')
279 then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
280 1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, expectedXPath, INCLUDE_ALL_DESCENDANTS)
283 def 'Get CM handles that has given module names'() {
284 when: 'the method to get cm handles is called'
285 objectUnderTest.getCmHandleIdsWithGivenModules(['sample-module-name'])
286 then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
287 1 * mockCpsAdminService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['sample-module-name'])
290 def 'Replace list content'() {
291 when: 'replace list content method is called with xpath and data nodes collection'
292 objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
293 then: 'the cps data service method to replace list content is invoked once with same parameters'
294 1 * mockCpsDataService.replaceListContent(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xpath', [new DataNode()], NO_TIMESTAMP);
297 def 'Delete data node via xPath'() {
298 when: 'Delete data node method is called with xpath as parameter'
299 objectUnderTest.deleteDataNode('sample dataNode xpath')
300 then: 'the cps data service method to delete data node is invoked once with the same xPath'
301 1 * mockCpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, 'sample dataNode xpath', NO_TIMESTAMP);
304 def 'Delete multiple data nodes via xPath'() {
305 when: 'Delete data nodes method is called with multiple xpaths as parameters'
306 objectUnderTest.deleteDataNodes(['xpath1', 'xpath2'])
307 then: 'the cps data service method to delete data nodes is invoked once with the same xPaths'
308 1 * mockCpsDataService.deleteDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, ['xpath1', 'xpath2'], NO_TIMESTAMP);