2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2022 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.CpsDataService
26 import org.onap.cps.api.CpsModuleService
27 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
28 import org.onap.cps.spi.CascadeDeleteAllowed
29 import org.onap.cps.spi.CpsDataPersistenceService
30 import org.onap.cps.spi.CpsAdminPersistenceService
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 spock.lang.Shared
38 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 mockCpsDataPersistenceService = Mock(CpsDataPersistenceService)
57 def mockCpsAdminPersistenceService = Mock(CpsAdminPersistenceService)
59 def objectUnderTest = new InventoryPersistenceImpl(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService,
60 mockCpsDataPersistenceService, mockCpsAdminPersistenceService)
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']"
70 def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"]),
71 new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
74 def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])]
77 def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
80 def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
82 def "Retrieve CmHandle using datanode with #scenario."() {
83 given: 'the cps data service returns a data node from the DMI registry'
84 def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
85 mockCpsDataPersistenceService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
86 when: 'retrieving the yang modelled cm handle'
87 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
88 then: 'the result has the correct id and service names'
89 result.id == cmHandleId
90 result.dmiServiceName == 'common service name'
91 result.dmiDataServiceName == 'data service name'
92 result.dmiModelServiceName == 'model service name'
93 and: 'the expected DMI properties'
94 result.dmiProperties == expectedDmiProperties
95 result.publicProperties == expectedPublicProperties
96 and: 'the state details are returned'
97 result.compositeState.cmHandleState == expectedCompositeState
98 where: 'the following parameters are used'
99 scenario | childDataNodes || expectedDmiProperties || expectedPublicProperties || expectedCompositeState
100 'no properties' | [] || [] || [] || null
101 'DMI and public properties' | childDataNodesForCmHandleWithAllProperties || [new YangModelCmHandle.Property("name1", "value1")] || [new YangModelCmHandle.Property("name2", "value2")] || null
102 'just DMI properties' | childDataNodesForCmHandleWithDMIProperties || [new YangModelCmHandle.Property("name1", "value1")] || [] || null
103 'just public properties' | childDataNodesForCmHandleWithPublicProperties || [] || [new YangModelCmHandle.Property("name2", "value2")] || null
104 'with state details' | childDataNodesForCmHandleWithState || [] || [] || CmHandleState.ADVISED
107 def "Retrieve CmHandle using datanode with invalid CmHandle id."() {
108 when: 'retrieving the yang modelled cm handle with an invalid id'
109 def result = objectUnderTest.getYangModelCmHandle('cm handle id with spaces')
110 then: 'a data validation exception is thrown'
111 thrown(DataValidationException)
112 and: 'the result is not returned'
116 def "Handling missing service names as null CPS-1043."() {
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 mockCpsDataPersistenceService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
120 when: 'retrieving the yang modelled cm handle'
121 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
122 then: 'the service names ae returned as null'
123 result.dmiServiceName == null
124 result.dmiDataServiceName == null
125 result.dmiModelServiceName == null
128 def 'Get a Cm Handle Composite State'() {
129 given: 'a valid cm handle id'
130 def cmHandleId = 'Some-Cm-Handle'
131 def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
132 and: 'cps data service returns a valid data node'
133 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
134 '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
135 when: 'get cm handle state is invoked'
136 def result = objectUnderTest.getCmHandleState(cmHandleId)
137 then: 'result has returned the correct cm handle state'
138 result.cmHandleState == CmHandleState.ADVISED
141 def 'Update Cm Handle with #scenario State'() {
142 given: 'a cm handle and a composite state'
143 def cmHandleId = 'Some-Cm-Handle'
144 def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
145 when: 'update cm handle state is invoked with the #scenario state'
146 objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
147 then: 'update node leaves is invoked with the correct params'
148 1 * mockCpsDataService.updateDataNodeAndDescendants('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
149 where: 'the following states are used'
150 scenario | cmHandleState || expectedJsonData
151 'READY' | CmHandleState.READY || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
152 'LOCKED' | CmHandleState.LOCKED || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
153 'DELETING' | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
156 def 'Update Cm Handles with #scenario States'() {
157 given: 'a map of cm handles composite states'
158 def compositeState1 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
159 def compositeState2 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
160 when: 'update cm handle state is invoked with the #scenario state'
161 def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2]
162 objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
163 then: 'update node leaves is invoked with the correct params'
164 1 * mockCpsDataService.updateDataNodesAndDescendants('NCMP-Admin', 'ncmp-dmi-registry', cmHandlesJsonDataMap, _ as OffsetDateTime)
165 where: 'the following states are used'
166 scenario | cmHandleState || cmHandlesJsonDataMap
167 '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"}}']
168 '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"}}']
169 '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"}}']
172 def 'Get module definitions'() {
173 given: 'cps module service returns a collection of module definitions'
174 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
175 mockCpsModuleService.getModuleDefinitionsByAnchorName('NFP-Operational','some-cmHandle-Id') >> moduleDefinitions
176 when: 'get module definitions by cmHandle is invoked'
177 def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
178 then: 'the returned result are the same module definitions as returned from the module service'
179 assert result == moduleDefinitions
182 def 'Get module references'() {
183 given: 'cps module service returns a collection of module references'
184 def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
185 mockCpsModuleService.getYangResourcesModuleReferences('NFP-Operational','some-cmHandle-Id') >> moduleReferences
186 when: 'get yang resources module references by cmHandle is invoked'
187 def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
188 then: 'the returned result is a collection of module definitions'
189 assert result == moduleReferences
192 def 'Save Cmhandle'() {
193 given: 'cmHandle represented as Yang Model'
194 def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: [])
195 when: 'the method to save cmhandle is called'
196 objectUnderTest.saveCmHandle(yangModelCmHandle)
197 then: 'the data service method to save list elements is called once'
198 1 * mockCpsDataService.saveListElements('NCMP-Admin','ncmp-dmi-registry','/dmi-registry',_,null) >> {
200 assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
205 def 'Save Multiple Cmhandles'() {
206 given: 'cm handles represented as Yang Model'
207 def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1')
208 def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2')
209 when: 'the cm handles are saved'
210 objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2])
211 then: 'CPS Data Service persists both cm handles as a batch'
212 1 * mockCpsDataService.saveListElementsBatch('NCMP-Admin','ncmp-dmi-registry','/dmi-registry',_,null) >> {
214 def jsonDataList = (args[3] as List)
215 (jsonDataList[0] as String).contains('cmhandle1')
216 (jsonDataList[0] as String).contains('cmhandle2')
221 def 'Delete list or list elements'() {
222 when: 'the method to delete list or list elements is called'
223 objectUnderTest.deleteListOrListElement('sample xPath')
224 then: 'the data service method to save list elements is called once'
225 1 * mockCpsDataService.deleteListOrListElement('NCMP-Admin','ncmp-dmi-registry','sample xPath',null)
228 def 'Delete schema set with a valid schema set name'() {
229 when: 'the method to delete schema set is called with valid schema set name'
230 objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
231 then: 'the module service to delete schemaSet is invoked once'
232 1 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
235 def 'Delete schema set with an invalid schema set name'() {
236 when: 'the method to delete schema set is called with an invalid schema set name'
237 objectUnderTest.deleteSchemaSetWithCascade('invalid SchemaSet name')
238 then: 'a data validation exception is thrown'
239 thrown(DataValidationException)
240 and: 'the module service to delete schemaSet is not called'
241 0 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'sampleSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
244 def 'Get data node via xPath'() {
245 when: 'the method to get data nodes is called'
246 objectUnderTest.getDataNode('sample xPath')
247 then: 'the data persistence service method to get data node is invoked once'
248 1 * mockCpsDataPersistenceService.getDataNode('NCMP-Admin','ncmp-dmi-registry','sample xPath', INCLUDE_ALL_DESCENDANTS)
251 def 'Get cmHandle data node'() {
252 given: 'expected xPath to get cmHandle data node'
253 def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']';
254 when: 'the method to get data nodes is called'
255 objectUnderTest.getCmHandleDataNode('sample cmHandleId')
256 then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
257 1 * mockCpsDataPersistenceService.getDataNode('NCMP-Admin','ncmp-dmi-registry',expectedXPath, INCLUDE_ALL_DESCENDANTS)
260 def 'Query anchors'() {
261 when: 'the method to query anchors is called'
262 objectUnderTest.queryAnchors(['sample-module-name'])
263 then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
264 1 * mockCpsAdminPersistenceService.queryAnchors('NFP-Operational',['sample-module-name'])
267 def 'Get anchors'() {
268 when: 'the method to get anchors with no parameters is called'
269 objectUnderTest.getAnchors()
270 then: 'the admin persistence service method to query anchors is invoked once with a specific dataspace name'
271 1 * mockCpsAdminPersistenceService.getAnchors('NFP-Operational')
274 def 'Replace list content'() {
275 when: 'replace list content method is called with xpath and data nodes collection'
276 objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
277 then: 'the cps data service method to replace list content is invoked once with same parameters'
278 1 * mockCpsDataService.replaceListContent('NCMP-Admin', 'ncmp-dmi-registry',
279 'sample xpath', [new DataNode()], NO_TIMESTAMP);
282 def 'Delete data node via xPath'() {
283 when: 'Delete data node method is called with xpath as parameter'
284 objectUnderTest.deleteDataNode('sample dataNode xpath')
285 then: 'the cps data service method to delete data node is invoked once with the same xPath'
286 1 * mockCpsDataService.deleteDataNode('NCMP-Admin', 'ncmp-dmi-registry',
287 'sample dataNode xpath', NO_TIMESTAMP);