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 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 'Get module definitions'() {
188 given: 'cps module service returns a collection of module definitions'
189 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
190 mockCpsModuleService.getModuleDefinitionsByAnchorName(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleDefinitions
191 when: 'get module definitions by cmHandle is invoked'
192 def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
193 then: 'the returned result are the same module definitions as returned from the module service'
194 assert result == moduleDefinitions
197 def 'Get module references'() {
198 given: 'cps module service returns a collection of module references'
199 def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
200 mockCpsModuleService.getYangResourcesModuleReferences(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleReferences
201 when: 'get yang resources module references by cmHandle is invoked'
202 def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
203 then: 'the returned result is a collection of module definitions'
204 assert result == moduleReferences
205 and: 'the CM Handle ID is validated'
206 1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id')
209 def 'Save Cmhandle'() {
210 given: 'cmHandle represented as Yang Model'
211 def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: [])
212 when: 'the method to save cmhandle is called'
213 objectUnderTest.saveCmHandle(yangModelCmHandle)
214 then: 'the data service method to save list elements is called once'
215 1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT,
218 assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
223 def 'Save Multiple Cmhandles'() {
224 given: 'cm handles represented as Yang Model'
225 def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1')
226 def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2')
227 when: 'the cm handles are saved'
228 objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2])
229 then: 'CPS Data Service persists both cm handles as a batch'
230 1 * mockCpsDataService.saveListElementsBatch(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
231 NCMP_DMI_REGISTRY_PARENT, _,null) >> {
233 def jsonDataList = (args[3] as List)
234 (jsonDataList[0] as String).contains('cmhandle1')
235 (jsonDataList[0] as String).contains('cmhandle2')
240 def 'Delete list or list elements'() {
241 when: 'the method to delete list or list elements is called'
242 objectUnderTest.deleteListOrListElement('sample xPath')
243 then: 'the data service method to save list elements is called once'
244 1 * mockCpsDataService.deleteListOrListElement(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath',null)
247 def 'Delete schema set with a valid schema set name'() {
248 when: 'the method to delete schema set is called with valid schema set name'
249 objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
250 then: 'the module service to delete schemaSet is invoked once'
251 1 * mockCpsModuleService.deleteSchemaSet(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
252 and: 'the schema set name is validated'
253 1 * mockCpsValidator.validateNameCharacters('validSchemaSetName')
256 def 'Delete multiple schema sets with valid schema set names'() {
257 when: 'the method to delete schema sets is called with valid schema set names'
258 objectUnderTest.deleteSchemaSetsWithCascade(['validSchemaSetName1', 'validSchemaSetName2'])
259 then: 'the module service to delete schema sets is invoked once'
260 1 * mockCpsModuleService.deleteSchemaSetsWithCascade(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['validSchemaSetName1', 'validSchemaSetName2'])
261 and: 'the schema set names are validated'
262 1 * mockCpsValidator.validateNameCharacters(['validSchemaSetName1', 'validSchemaSetName2'])
265 def 'Get data node via xPath'() {
266 when: 'the method to get data nodes is called'
267 objectUnderTest.getDataNode('sample xPath')
268 then: 'the data persistence service method to get data node is invoked once'
269 1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath', INCLUDE_ALL_DESCENDANTS)
272 def 'Get cmHandle data node'() {
273 given: 'expected xPath to get cmHandle data node'
274 def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']';
275 when: 'the method to get data nodes is called'
276 objectUnderTest.getCmHandleDataNode('sample cmHandleId')
277 then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
278 1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, expectedXPath, INCLUDE_ALL_DESCENDANTS)
281 def 'Get CM handles that has given module names'() {
282 when: 'the method to get cm handles is called'
283 objectUnderTest.getCmHandleIdsWithGivenModules(['sample-module-name'])
284 then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
285 1 * mockCpsAnchorService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['sample-module-name'])
288 def 'Replace list content'() {
289 when: 'replace list content method is called with xpath and data nodes collection'
290 objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
291 then: 'the cps data service method to replace list content is invoked once with same parameters'
292 1 * mockCpsDataService.replaceListContent(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xpath', [new DataNode()], NO_TIMESTAMP);
295 def 'Delete data node via xPath'() {
296 when: 'Delete data node method is called with xpath as parameter'
297 objectUnderTest.deleteDataNode('sample dataNode xpath')
298 then: 'the cps data service method to delete data node is invoked once with the same xPath'
299 1 * mockCpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, 'sample dataNode xpath', NO_TIMESTAMP);
302 def 'Delete multiple data nodes via xPath'() {
303 when: 'Delete data nodes method is called with multiple xpaths as parameters'
304 objectUnderTest.deleteDataNodes(['xpath1', 'xpath2'])
305 then: 'the cps data service method to delete data nodes is invoked once with the same xPaths'
306 1 * mockCpsDataService.deleteDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, ['xpath1', 'xpath2'], NO_TIMESTAMP);