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.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.model.DataNode
32 import org.onap.cps.spi.model.ModuleDefinition
33 import org.onap.cps.spi.model.ModuleReference
34 import org.onap.cps.utils.JsonObjectMapper
35 import org.onap.cps.spi.utils.CpsValidator
36 import spock.lang.Shared
37 import spock.lang.Specification
38 import java.time.OffsetDateTime
39 import java.time.ZoneOffset
40 import java.time.format.DateTimeFormatter
42 import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NO_TIMESTAMP
43 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
45 class InventoryPersistenceImplSpec extends Specification {
47 def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
49 def mockCpsDataService = Mock(CpsDataService)
51 def mockCpsModuleService = Mock(CpsModuleService)
53 def mockCpsAdminService = Mock(CpsAdminService)
55 def mockCpsValidator = Mock(CpsValidator)
57 def objectUnderTest = new InventoryPersistenceImpl(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService,
58 mockCpsAdminService, mockCpsValidator)
60 def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
61 .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
63 def cmHandleId = 'some-cm-handle'
64 def leaves = ["dmi-service-name":"common service name","dmi-data-service-name":"data service name","dmi-model-service-name":"model service name"]
65 def xpath = "/dmi-registry/cm-handles[@id='some-cm-handle']"
68 def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"]),
69 new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
72 def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])]
75 def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
78 def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
80 def "Retrieve CmHandle using datanode with #scenario."() {
81 given: 'the cps data service returns a data node from the DMI registry'
82 def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
83 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
84 when: 'retrieving the yang modelled cm handle'
85 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
86 then: 'the result has the correct id and service names'
87 result.id == cmHandleId
88 result.dmiServiceName == 'common service name'
89 result.dmiDataServiceName == 'data service name'
90 result.dmiModelServiceName == 'model service name'
91 and: 'the expected DMI properties'
92 result.dmiProperties == expectedDmiProperties
93 result.publicProperties == expectedPublicProperties
94 and: 'the state details are returned'
95 result.compositeState.cmHandleState == expectedCompositeState
96 and: 'the CM Handle ID is validated'
97 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
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 "Handling missing service names as null."() {
108 given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes'
109 def dataNode = new DataNode(childDataNodes:[], leaves: [:])
110 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
111 when: 'retrieving the yang modelled cm handle'
112 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
113 then: 'the service names are returned as null'
114 result.dmiServiceName == null
115 result.dmiDataServiceName == null
116 result.dmiModelServiceName == null
117 and: 'the CM Handle ID is validated'
118 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
121 def 'Get a Cm Handle Composite State'() {
122 given: 'a valid cm handle id'
123 def cmHandleId = 'Some-Cm-Handle'
124 def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
125 and: 'cps data service returns a valid data node'
126 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
127 '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
128 when: 'get cm handle state is invoked'
129 def result = objectUnderTest.getCmHandleState(cmHandleId)
130 then: 'result has returned the correct cm handle state'
131 result.cmHandleState == CmHandleState.ADVISED
132 and: 'the CM Handle ID is validated'
133 1 * mockCpsValidator.validateNameCharacters(cmHandleId)
136 def 'Update Cm Handle with #scenario State'() {
137 given: 'a cm handle and a composite state'
138 def cmHandleId = 'Some-Cm-Handle'
139 def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
140 when: 'update cm handle state is invoked with the #scenario state'
141 objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
142 then: 'update node leaves is invoked with the correct params'
143 1 * mockCpsDataService.updateDataNodeAndDescendants('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
144 where: 'the following states are used'
145 scenario | cmHandleState || expectedJsonData
146 'READY' | CmHandleState.READY || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
147 'LOCKED' | CmHandleState.LOCKED || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
148 'DELETING' | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
151 def 'Update Cm Handles with #scenario States'() {
152 given: 'a map of cm handles composite states'
153 def compositeState1 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
154 def compositeState2 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
155 when: 'update cm handle state is invoked with the #scenario state'
156 def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2]
157 objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
158 then: 'update node leaves is invoked with the correct params'
159 1 * mockCpsDataService.updateDataNodesAndDescendants('NCMP-Admin', 'ncmp-dmi-registry', cmHandlesJsonDataMap, _ as OffsetDateTime)
160 where: 'the following states are used'
161 scenario | cmHandleState || cmHandlesJsonDataMap
162 '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"}}']
163 '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"}}']
164 '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"}}']
167 def 'Get module definitions'() {
168 given: 'cps module service returns a collection of module definitions'
169 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
170 mockCpsModuleService.getModuleDefinitionsByAnchorName('NFP-Operational','some-cmHandle-Id') >> moduleDefinitions
171 when: 'get module definitions by cmHandle is invoked'
172 def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
173 then: 'the returned result are the same module definitions as returned from the module service'
174 assert result == moduleDefinitions
177 def 'Get module references'() {
178 given: 'cps module service returns a collection of module references'
179 def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
180 mockCpsModuleService.getYangResourcesModuleReferences('NFP-Operational','some-cmHandle-Id') >> moduleReferences
181 when: 'get yang resources module references by cmHandle is invoked'
182 def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
183 then: 'the returned result is a collection of module definitions'
184 assert result == moduleReferences
185 and: 'the CM Handle ID is validated'
186 1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id')
189 def 'Save Cmhandle'() {
190 given: 'cmHandle represented as Yang Model'
191 def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: [])
192 when: 'the method to save cmhandle is called'
193 objectUnderTest.saveCmHandle(yangModelCmHandle)
194 then: 'the data service method to save list elements is called once'
195 1 * mockCpsDataService.saveListElements('NCMP-Admin','ncmp-dmi-registry','/dmi-registry',_,null) >> {
197 assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
202 def 'Save Multiple Cmhandles'() {
203 given: 'cm handles represented as Yang Model'
204 def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1')
205 def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2')
206 when: 'the cm handles are saved'
207 objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2])
208 then: 'CPS Data Service persists both cm handles as a batch'
209 1 * mockCpsDataService.saveListElementsBatch('NCMP-Admin','ncmp-dmi-registry','/dmi-registry',_,null) >> {
211 def jsonDataList = (args[3] as List)
212 (jsonDataList[0] as String).contains('cmhandle1')
213 (jsonDataList[0] as String).contains('cmhandle2')
218 def 'Delete list or list elements'() {
219 when: 'the method to delete list or list elements is called'
220 objectUnderTest.deleteListOrListElement('sample xPath')
221 then: 'the data service method to save list elements is called once'
222 1 * mockCpsDataService.deleteListOrListElement('NCMP-Admin','ncmp-dmi-registry','sample xPath',null)
225 def 'Delete schema set with a valid schema set name'() {
226 when: 'the method to delete schema set is called with valid schema set name'
227 objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
228 then: 'the module service to delete schemaSet is invoked once'
229 1 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
230 and: 'the CM Handle ID is validated'
231 1 * mockCpsValidator.validateNameCharacters('validSchemaSetName')
234 def 'Get data node via xPath'() {
235 when: 'the method to get data nodes is called'
236 objectUnderTest.getDataNode('sample xPath')
237 then: 'the data persistence service method to get data node is invoked once'
238 1 * mockCpsDataService.getDataNode('NCMP-Admin','ncmp-dmi-registry','sample xPath', INCLUDE_ALL_DESCENDANTS)
241 def 'Get cmHandle data node'() {
242 given: 'expected xPath to get cmHandle data node'
243 def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']';
244 when: 'the method to get data nodes is called'
245 objectUnderTest.getCmHandleDataNode('sample cmHandleId')
246 then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
247 1 * mockCpsDataService.getDataNode('NCMP-Admin','ncmp-dmi-registry',expectedXPath, INCLUDE_ALL_DESCENDANTS)
250 def 'Get CM handles that has given module names'() {
251 when: 'the method to get cm handles is called'
252 objectUnderTest.getCmHandleIdsWithGivenModules(['sample-module-name'])
253 then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
254 1 * mockCpsAdminService.queryAnchorNames('NFP-Operational',['sample-module-name'])
257 def 'Replace list content'() {
258 when: 'replace list content method is called with xpath and data nodes collection'
259 objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
260 then: 'the cps data service method to replace list content is invoked once with same parameters'
261 1 * mockCpsDataService.replaceListContent('NCMP-Admin', 'ncmp-dmi-registry',
262 'sample xpath', [new DataNode()], NO_TIMESTAMP);
265 def 'Delete data node via xPath'() {
266 when: 'Delete data node method is called with xpath as parameter'
267 objectUnderTest.deleteDataNode('sample dataNode xpath')
268 then: 'the cps data service method to delete data node is invoked once with the same xPath'
269 1 * mockCpsDataService.deleteDataNode('NCMP-Admin', 'ncmp-dmi-registry',
270 'sample dataNode xpath', NO_TIMESTAMP);
273 def 'Delete multiple data nodes via xPath'() {
274 when: 'Delete data nodes method is called with multiple xpaths as parameters'
275 objectUnderTest.deleteDataNodes(['xpath1', 'xpath2'])
276 then: 'the cps data service method to delete data nodes is invoked once with the same xPaths'
277 1 * mockCpsDataService.deleteDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
278 ['xpath1', 'xpath2'], NO_TIMESTAMP);