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
46 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
48 class InventoryPersistenceSpec extends Specification {
50 def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
52 def mockCpsDataService = Mock(CpsDataService)
54 def mockCpsModuleService = Mock(CpsModuleService)
56 def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService)
58 def mockCpsAdminPersistenceService = Mock(CpsAdminPersistenceService)
60 def objectUnderTest = new InventoryPersistence(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService,
61 mockCpsDataPersistenceService, mockCpsAdminPersistenceService)
63 def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
64 .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
66 def cmHandleId = 'some-cm-handle'
67 def leaves = ["dmi-service-name":"common service name","dmi-data-service-name":"data service name","dmi-model-service-name":"model service name"]
68 def xpath = "/dmi-registry/cm-handles[@id='some-cm-handle']"
71 def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"]),
72 new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
75 def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])]
78 def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
81 def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
84 def static sampleDataNodes = [new DataNode()]
86 def "Retrieve CmHandle using datanode with #scenario."() {
87 given: 'the cps data service returns a data node from the DMI registry'
88 def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
89 mockCpsDataPersistenceService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
90 when: 'retrieving the yang modelled cm handle'
91 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
92 then: 'the result has the correct id and service names'
93 result.id == cmHandleId
94 result.dmiServiceName == 'common service name'
95 result.dmiDataServiceName == 'data service name'
96 result.dmiModelServiceName == 'model service name'
97 and: 'the expected DMI properties'
98 result.dmiProperties == expectedDmiProperties
99 result.publicProperties == expectedPublicProperties
100 and: 'the state details are returned'
101 result.compositeState.cmHandleState == expectedCompositeState
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 "Retrieve CmHandle using datanode with invalid CmHandle id."() {
112 when: 'retrieving the yang modelled cm handle with an invalid id'
113 def result = objectUnderTest.getYangModelCmHandle('cm handle id with spaces')
114 then: 'a data validation exception is thrown'
115 thrown(DataValidationException)
116 and: 'the result is not returned'
120 def "Handling missing service names as null CPS-1043."() {
121 given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes'
122 def dataNode = new DataNode(childDataNodes:[], leaves: [:])
123 mockCpsDataPersistenceService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
124 when: 'retrieving the yang modelled cm handle'
125 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
126 then: 'the service names ae returned as null'
127 result.dmiServiceName == null
128 result.dmiDataServiceName == null
129 result.dmiModelServiceName == null
132 def 'Get a Cm Handle Composite State'() {
133 given: 'a valid cm handle id'
134 def cmHandleId = 'Some-Cm-Handle'
135 def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
136 and: 'cps data service returns a valid data node'
137 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
138 '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
139 when: 'get cm handle state is invoked'
140 def result = objectUnderTest.getCmHandleState(cmHandleId)
141 then: 'result has returned the correct cm handle state'
142 result.cmHandleState == CmHandleState.ADVISED
145 def 'Update Cm Handle with #scenario State'() {
146 given: 'a cm handle and a composite state'
147 def cmHandleId = 'Some-Cm-Handle'
148 def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
149 when: 'update cm handle state is invoked with the #scenario state'
150 objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
151 then: 'update node leaves is invoked with the correct params'
152 1 * mockCpsDataService.replaceNodeTree('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
153 where: 'the following states are used'
154 scenario | cmHandleState || expectedJsonData
155 'READY' | CmHandleState.READY || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
156 'LOCKED' | CmHandleState.LOCKED || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
157 'DELETING' | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
160 def 'Get Cm Handles By State'() {
161 given: 'a cm handle state to query'
162 def cmHandleState = CmHandleState.ADVISED
163 and: 'cps data service returns a list of data nodes'
164 mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
165 '//state[@cm-handle-state="ADVISED"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
166 when: 'get cm handles by state is invoked'
167 def result = objectUnderTest.getCmHandlesByState(cmHandleState)
168 then: 'the returned result is a list of data nodes returned by cps data service'
169 assert result == sampleDataNodes
172 def 'Get Cm Handles By State and Cm-Handle Id'() {
173 given: 'a cm handle state to query'
174 def cmHandleState = CmHandleState.READY
175 and: 'cps data service returns a list of data nodes'
176 mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
177 '//cm-handles[@id=\'some-cm-handle\']/state[@cm-handle-state="'+ 'READY'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
178 when: 'get cm handles by state and id is invoked'
179 def result = objectUnderTest.getCmHandlesByIdAndState(cmHandleId, cmHandleState)
180 then: 'the returned result is a list of data nodes returned by cps data service'
181 assert result == sampleDataNodes
184 def 'Get Cm Handles By Operational Sync State : UNSYNCHRONIZED'() {
185 given: 'a cm handle state to query'
186 def cmHandleState = CmHandleState.READY
187 and: 'cps data service returns a list of data nodes'
188 mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
189 '//state/datastores/operational[@sync-state="'+'UNSYNCHRONIZED'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
190 when: 'get cm handles by operational sync state as UNSYNCHRONIZED is invoked'
191 def result = objectUnderTest.getCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED)
192 then: 'the returned result is a list of data nodes returned by cps data service'
193 assert result == sampleDataNodes
196 def 'Retrieve cm handle by cps path '() {
197 given: 'a cm handle state to query based on the cps path'
198 def cmHandleDataNode = new DataNode(xpath: 'xpath', leaves: ['cm-handle-state': 'LOCKED'])
199 def cpsPath = '//cps-path'
200 and: 'cps data service returns a valid data node'
201 mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
202 cpsPath, INCLUDE_ALL_DESCENDANTS)
203 >> Arrays.asList(cmHandleDataNode)
204 when: 'get cm handles by cps path is invoked'
205 def result = objectUnderTest.getCmHandleDataNodesByCpsPath(cpsPath, INCLUDE_ALL_DESCENDANTS)
206 then: 'the returned result is a list of data nodes returned by cps data service'
207 assert result.contains(cmHandleDataNode)
210 def 'Get module definitions'() {
211 given: 'cps module service returns a collection of module definitions'
212 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
213 mockCpsModuleService.getModuleDefinitionsByAnchorName('NFP-Operational','some-cmHandle-Id') >> moduleDefinitions
214 when: 'get module definitions by cmHandle is invoked'
215 def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
216 then: 'the returned result are the same module definitions as returned from the module service'
217 assert result == moduleDefinitions
220 def 'Get module references'() {
221 given: 'cps module service returns a collection of module references'
222 def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
223 mockCpsModuleService.getYangResourcesModuleReferences('NFP-Operational','some-cmHandle-Id') >> moduleReferences
224 when: 'get yang resources module references by cmHandle is invoked'
225 def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
226 then: 'the returned result is a collection of module definitions'
227 assert result == moduleReferences
230 def 'Save list elements'() {
231 when: 'the method to save list elements is called'
232 objectUnderTest.saveListElements('sample Json data')
233 then: 'the data service method to save list elements is called once'
234 1 * mockCpsDataService.saveListElements('NCMP-Admin','ncmp-dmi-registry','/dmi-registry','sample Json data',null)
237 def 'Delete list or list elements'() {
238 when: 'the method to delete list or list elements is called'
239 objectUnderTest.deleteListOrListElement('sample xPath')
240 then: 'the data service method to save list elements is called once'
241 1 * mockCpsDataService.deleteListOrListElement('NCMP-Admin','ncmp-dmi-registry','sample xPath',null)
244 def 'Delete schema set with a valid schema set name'() {
245 when: 'the method to delete schema set is called with valid schema set name'
246 objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
247 then: 'the module service to delete schemaSet is invoked once'
248 1 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
251 def 'Delete schema set with an invalid schema set name'() {
252 when: 'the method to delete schema set is called with an invalid schema set name'
253 objectUnderTest.deleteSchemaSetWithCascade('invalid SchemaSet name')
254 then: 'a data validation exception is thrown'
255 thrown(DataValidationException)
256 and: 'the module service to delete schemaSet is not called'
257 0 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'sampleSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
260 def 'Query data nodes via cpsPath'() {
261 when: 'the method to query data nodes is called'
262 objectUnderTest.queryDataNodes('sample cpsPath')
263 then: 'the data persistence service method to query data nodes is invoked once'
264 1 * mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin','ncmp-dmi-registry','sample cpsPath', INCLUDE_ALL_DESCENDANTS)
267 def 'Get data node via xPath'() {
268 when: 'the method to get data nodes is called'
269 objectUnderTest.getDataNode('sample xPath')
270 then: 'the data persistence service method to get data node is invoked once'
271 1 * mockCpsDataPersistenceService.getDataNode('NCMP-Admin','ncmp-dmi-registry','sample xPath', INCLUDE_ALL_DESCENDANTS)
274 def 'Get cmHandle data node'() {
275 given: 'expected xPath to get cmHandle data node'
276 def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']';
277 when: 'the method to get data nodes is called'
278 objectUnderTest.getCmHandleDataNode('sample cmHandleId')
279 then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
280 1 * mockCpsDataPersistenceService.getDataNode('NCMP-Admin','ncmp-dmi-registry',expectedXPath, INCLUDE_ALL_DESCENDANTS)
283 def 'Query anchors'() {
284 when: 'the method to query anchors is called'
285 objectUnderTest.queryAnchors(['sample-module-name'])
286 then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
287 1 * mockCpsAdminPersistenceService.queryAnchors('NFP-Operational',['sample-module-name'])
290 def 'Get anchors'() {
291 when: 'the method to get anchors with no parameters is called'
292 objectUnderTest.getAnchors()
293 then: 'the admin persistence service method to query anchors is invoked once with a specific dataspace name'
294 1 * mockCpsAdminPersistenceService.getAnchors('NFP-Operational')
297 def 'Replace list content'() {
298 when: 'replace list content method is called with xpath and data nodes collection'
299 objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
300 then: 'the cps data service method to replace list content is invoked once with same parameters'
301 1 * mockCpsDataService.replaceListContent('NCMP-Admin', 'ncmp-dmi-registry',
302 'sample xpath', [new DataNode()], NO_TIMESTAMP);
305 def 'Delete data node via xPath'() {
306 when: 'Delete data node method is called with xpath as parameter'
307 objectUnderTest.deleteDataNode('sample dataNode xpath')
308 then: 'the cps data service method to delete data node is invoked once with the same xPath'
309 1 * mockCpsDataService.deleteDataNode('NCMP-Admin', 'ncmp-dmi-registry',
310 'sample dataNode xpath', NO_TIMESTAMP);