2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2022-2023 Nordix Foundation
4 * Modifications Copyright (C) 2022 Bell Canada
5 * ================================================================================
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
18 * SPDX-License-Identifier: Apache-2.0
19 * ============LICENSE_END=========================================================
22 package org.onap.cps.ncmp.api.inventory
24 import com.fasterxml.jackson.databind.ObjectMapper
25 import org.onap.cps.api.CpsAdminService
26 import org.onap.cps.api.CpsDataService
27 import org.onap.cps.api.CpsModuleService
28 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
29 import org.onap.cps.spi.CascadeDeleteAllowed
30 import org.onap.cps.spi.FetchDescendantsOption
31 import org.onap.cps.spi.exceptions.DataValidationException
32 import org.onap.cps.spi.model.DataNode
33 import org.onap.cps.spi.model.ModuleDefinition
34 import org.onap.cps.spi.model.ModuleReference
35 import org.onap.cps.utils.JsonObjectMapper
36 import org.onap.cps.spi.utils.CpsValidator
37 import spock.lang.Shared
38 import spock.lang.Specification
39 import java.time.OffsetDateTime
40 import java.time.ZoneOffset
41 import java.time.format.DateTimeFormatter
42 import java.util.stream.Collectors
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.getDataNode('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.getDataNode('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.getDataNodes('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 "Handling name validation errors in getYangModelCmHandles."() {
138 given: 'the cps data service returns one of two data nodes from the DMI registry with empty leaf attributes'
139 mockCpsValidator.validateNameCharacters(cmHandleId) >> {throw new DataValidationException('some message', 'some detail')}
141 objectUnderTest.getYangModelCmHandle(cmHandleId)
142 then: 'exception is thrown'
143 thrown(DataValidationException)
146 def 'Get a Cm Handle Composite State'() {
147 given: 'a valid cm handle id'
148 def cmHandleId = 'Some-Cm-Handle'
149 def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
150 and: 'cps data service returns a valid data node'
151 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
152 '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
153 when: 'get cm handle state is invoked'
154 def result = objectUnderTest.getCmHandleState(cmHandleId)
155 then: 'result has returned the correct cm handle state'
156 result.cmHandleState == CmHandleState.ADVISED
157 and: 'the CM Handle ID is validated'
158 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
161 def 'Update Cm Handle with #scenario State'() {
162 given: 'a cm handle and a composite state'
163 def cmHandleId = 'Some-Cm-Handle'
164 def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
165 when: 'update cm handle state is invoked with the #scenario state'
166 objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
167 then: 'update node leaves is invoked with the correct params'
168 1 * mockCpsDataService.updateDataNodeAndDescendants('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
169 where: 'the following states are used'
170 scenario | cmHandleState || expectedJsonData
171 'READY' | CmHandleState.READY || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
172 'LOCKED' | CmHandleState.LOCKED || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
173 'DELETING' | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
176 def 'Update Cm Handles with #scenario States'() {
177 given: 'a map of cm handles composite states'
178 def compositeState1 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
179 def compositeState2 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
180 when: 'update cm handle state is invoked with the #scenario state'
181 def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2]
182 objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
183 then: 'update node leaves is invoked with the correct params'
184 1 * mockCpsDataService.updateDataNodesAndDescendants('NCMP-Admin', 'ncmp-dmi-registry', cmHandlesJsonDataMap, _ as OffsetDateTime)
185 where: 'the following states are used'
186 scenario | cmHandleState || cmHandlesJsonDataMap
187 '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"}}']
188 '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"}}']
189 '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"}}']
192 def 'Get module definitions'() {
193 given: 'cps module service returns a collection of module definitions'
194 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
195 mockCpsModuleService.getModuleDefinitionsByAnchorName('NFP-Operational','some-cmHandle-Id') >> moduleDefinitions
196 when: 'get module definitions by cmHandle is invoked'
197 def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
198 then: 'the returned result are the same module definitions as returned from the module service'
199 assert result == moduleDefinitions
202 def 'Get module references'() {
203 given: 'cps module service returns a collection of module references'
204 def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
205 mockCpsModuleService.getYangResourcesModuleReferences('NFP-Operational','some-cmHandle-Id') >> moduleReferences
206 when: 'get yang resources module references by cmHandle is invoked'
207 def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
208 then: 'the returned result is a collection of module definitions'
209 assert result == moduleReferences
210 and: 'the CM Handle ID is validated'
211 1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id')
214 def 'Save Cmhandle'() {
215 given: 'cmHandle represented as Yang Model'
216 def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: [])
217 when: 'the method to save cmhandle is called'
218 objectUnderTest.saveCmHandle(yangModelCmHandle)
219 then: 'the data service method to save list elements is called once'
220 1 * mockCpsDataService.saveListElements('NCMP-Admin','ncmp-dmi-registry','/dmi-registry',_,null) >> {
222 assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
227 def 'Save Multiple Cmhandles'() {
228 given: 'cm handles represented as Yang Model'
229 def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1')
230 def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2')
231 when: 'the cm handles are saved'
232 objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2])
233 then: 'CPS Data Service persists both cm handles as a batch'
234 1 * mockCpsDataService.saveListElementsBatch('NCMP-Admin','ncmp-dmi-registry','/dmi-registry',_,null) >> {
236 def jsonDataList = (args[3] as List)
237 (jsonDataList[0] as String).contains('cmhandle1')
238 (jsonDataList[0] as String).contains('cmhandle2')
243 def 'Delete list or list elements'() {
244 when: 'the method to delete list or list elements is called'
245 objectUnderTest.deleteListOrListElement('sample xPath')
246 then: 'the data service method to save list elements is called once'
247 1 * mockCpsDataService.deleteListOrListElement('NCMP-Admin','ncmp-dmi-registry','sample xPath',null)
250 def 'Delete schema set with a valid schema set name'() {
251 when: 'the method to delete schema set is called with valid schema set name'
252 objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
253 then: 'the module service to delete schemaSet is invoked once'
254 1 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
255 and: 'the CM Handle ID is validated'
256 1 * mockCpsValidator.validateNameCharacters('validSchemaSetName')
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.getDataNode('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.getDataNode('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);