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