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.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
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 mockCpsValidator = Mock(CpsValidator)
61 def objectUnderTest = new InventoryPersistenceImpl(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService,
62 mockCpsDataPersistenceService, mockCpsAdminPersistenceService, mockCpsValidator)
64 def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
65 .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
67 def cmHandleId = 'some-cm-handle'
68 def leaves = ["dmi-service-name":"common service name","dmi-data-service-name":"data service name","dmi-model-service-name":"model service name"]
69 def xpath = "/dmi-registry/cm-handles[@id='some-cm-handle']"
72 def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"]),
73 new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
76 def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])]
79 def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
82 def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
84 def "Retrieve CmHandle using datanode with #scenario."() {
85 given: 'the cps data service returns a data node from the DMI registry'
86 def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
87 mockCpsDataPersistenceService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
88 when: 'retrieving the yang modelled cm handle'
89 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
90 then: 'the result has the correct id and service names'
91 result.id == cmHandleId
92 result.dmiServiceName == 'common service name'
93 result.dmiDataServiceName == 'data service name'
94 result.dmiModelServiceName == 'model service name'
95 and: 'the expected DMI properties'
96 result.dmiProperties == expectedDmiProperties
97 result.publicProperties == expectedPublicProperties
98 and: 'the state details are returned'
99 result.compositeState.cmHandleState == expectedCompositeState
100 and: 'the CM Handle ID is validated'
101 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
102 where: 'the following parameters are used'
103 scenario | childDataNodes || expectedDmiProperties || expectedPublicProperties || expectedCompositeState
104 'no properties' | [] || [] || [] || null
105 'DMI and public properties' | childDataNodesForCmHandleWithAllProperties || [new YangModelCmHandle.Property("name1", "value1")] || [new YangModelCmHandle.Property("name2", "value2")] || null
106 'just DMI properties' | childDataNodesForCmHandleWithDMIProperties || [new YangModelCmHandle.Property("name1", "value1")] || [] || null
107 'just public properties' | childDataNodesForCmHandleWithPublicProperties || [] || [new YangModelCmHandle.Property("name2", "value2")] || null
108 'with state details' | childDataNodesForCmHandleWithState || [] || [] || CmHandleState.ADVISED
111 def "Handling missing service names as null."() {
112 given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes'
113 def dataNode = new DataNode(childDataNodes:[], leaves: [:])
114 mockCpsDataPersistenceService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
115 when: 'retrieving the yang modelled cm handle'
116 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
117 then: 'the service names are returned as null'
118 result.dmiServiceName == null
119 result.dmiDataServiceName == null
120 result.dmiModelServiceName == null
121 and: 'the CM Handle ID is validated'
122 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
125 def 'Get a Cm Handle Composite State'() {
126 given: 'a valid cm handle id'
127 def cmHandleId = 'Some-Cm-Handle'
128 def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
129 and: 'cps data service returns a valid data node'
130 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
131 '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
132 when: 'get cm handle state is invoked'
133 def result = objectUnderTest.getCmHandleState(cmHandleId)
134 then: 'result has returned the correct cm handle state'
135 result.cmHandleState == CmHandleState.ADVISED
136 and: 'the CM Handle ID is validated'
137 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
140 def 'Update Cm Handle with #scenario State'() {
141 given: 'a cm handle and a composite state'
142 def cmHandleId = 'Some-Cm-Handle'
143 def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
144 when: 'update cm handle state is invoked with the #scenario state'
145 objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
146 then: 'update node leaves is invoked with the correct params'
147 1 * mockCpsDataService.updateDataNodeAndDescendants('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
148 where: 'the following states are used'
149 scenario | cmHandleState || expectedJsonData
150 'READY' | CmHandleState.READY || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
151 'LOCKED' | CmHandleState.LOCKED || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
152 'DELETING' | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
155 def 'Update Cm Handles with #scenario States'() {
156 given: 'a map of cm handles composite states'
157 def compositeState1 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
158 def compositeState2 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
159 when: 'update cm handle state is invoked with the #scenario state'
160 def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2]
161 objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
162 then: 'update node leaves is invoked with the correct params'
163 1 * mockCpsDataService.updateDataNodesAndDescendants('NCMP-Admin', 'ncmp-dmi-registry', cmHandlesJsonDataMap, _ as OffsetDateTime)
164 where: 'the following states are used'
165 scenario | cmHandleState || cmHandlesJsonDataMap
166 '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"}}']
167 '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"}}']
168 '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"}}']
171 def 'Get module definitions'() {
172 given: 'cps module service returns a collection of module definitions'
173 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
174 mockCpsModuleService.getModuleDefinitionsByAnchorName('NFP-Operational','some-cmHandle-Id') >> moduleDefinitions
175 when: 'get module definitions by cmHandle is invoked'
176 def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
177 then: 'the returned result are the same module definitions as returned from the module service'
178 assert result == moduleDefinitions
181 def 'Get module references'() {
182 given: 'cps module service returns a collection of module references'
183 def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
184 mockCpsModuleService.getYangResourcesModuleReferences('NFP-Operational','some-cmHandle-Id') >> moduleReferences
185 when: 'get yang resources module references by cmHandle is invoked'
186 def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
187 then: 'the returned result is a collection of module definitions'
188 assert result == moduleReferences
189 and: 'the CM Handle ID is validated'
190 1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id')
193 def 'Save Cmhandle'() {
194 given: 'cmHandle represented as Yang Model'
195 def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: [])
196 when: 'the method to save cmhandle is called'
197 objectUnderTest.saveCmHandle(yangModelCmHandle)
198 then: 'the data service method to save list elements is called once'
199 1 * mockCpsDataService.saveListElements('NCMP-Admin','ncmp-dmi-registry','/dmi-registry',_,null) >> {
201 assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
206 def 'Save Multiple Cmhandles'() {
207 given: 'cm handles represented as Yang Model'
208 def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1')
209 def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2')
210 when: 'the cm handles are saved'
211 objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2])
212 then: 'CPS Data Service persists both cm handles as a batch'
213 1 * mockCpsDataService.saveListElementsBatch('NCMP-Admin','ncmp-dmi-registry','/dmi-registry',_,null) >> {
215 def jsonDataList = (args[3] as List)
216 (jsonDataList[0] as String).contains('cmhandle1')
217 (jsonDataList[0] as String).contains('cmhandle2')
222 def 'Delete list or list elements'() {
223 when: 'the method to delete list or list elements is called'
224 objectUnderTest.deleteListOrListElement('sample xPath')
225 then: 'the data service method to save list elements is called once'
226 1 * mockCpsDataService.deleteListOrListElement('NCMP-Admin','ncmp-dmi-registry','sample xPath',null)
229 def 'Delete schema set with a valid schema set name'() {
230 when: 'the method to delete schema set is called with valid schema set name'
231 objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
232 then: 'the module service to delete schemaSet is invoked once'
233 1 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
234 and: 'the CM Handle ID is validated'
235 1 * mockCpsValidator.validateNameCharacters('validSchemaSetName')
238 def 'Get data node via xPath'() {
239 when: 'the method to get data nodes is called'
240 objectUnderTest.getDataNode('sample xPath')
241 then: 'the data persistence service method to get data node is invoked once'
242 1 * mockCpsDataPersistenceService.getDataNode('NCMP-Admin','ncmp-dmi-registry','sample xPath', INCLUDE_ALL_DESCENDANTS)
245 def 'Get cmHandle data node'() {
246 given: 'expected xPath to get cmHandle data node'
247 def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']';
248 when: 'the method to get data nodes is called'
249 objectUnderTest.getCmHandleDataNode('sample cmHandleId')
250 then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
251 1 * mockCpsDataPersistenceService.getDataNode('NCMP-Admin','ncmp-dmi-registry',expectedXPath, INCLUDE_ALL_DESCENDANTS)
254 def 'Query anchors'() {
255 when: 'the method to query anchors is called'
256 objectUnderTest.queryAnchors(['sample-module-name'])
257 then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
258 1 * mockCpsAdminPersistenceService.queryAnchors('NFP-Operational',['sample-module-name'])
261 def 'Get anchors'() {
262 when: 'the method to get anchors with no parameters is called'
263 objectUnderTest.getAnchors()
264 then: 'the admin persistence service method to query anchors is invoked once with a specific dataspace name'
265 1 * mockCpsAdminPersistenceService.getAnchors('NFP-Operational')
268 def 'Replace list content'() {
269 when: 'replace list content method is called with xpath and data nodes collection'
270 objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
271 then: 'the cps data service method to replace list content is invoked once with same parameters'
272 1 * mockCpsDataService.replaceListContent('NCMP-Admin', 'ncmp-dmi-registry',
273 'sample xpath', [new DataNode()], NO_TIMESTAMP);
276 def 'Delete data node via xPath'() {
277 when: 'Delete data node method is called with xpath as parameter'
278 objectUnderTest.deleteDataNode('sample dataNode xpath')
279 then: 'the cps data service method to delete data node is invoked once with the same xPath'
280 1 * mockCpsDataService.deleteDataNode('NCMP-Admin', 'ncmp-dmi-registry',
281 'sample dataNode xpath', NO_TIMESTAMP);