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 Cmhandle'() {
231 given: 'cmHandle represented as Yang Model'
232 def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: [])
233 when: 'the method to save cmhandle is called'
234 objectUnderTest.saveCmHandle(yangModelCmHandle)
235 then: 'the data service method to save list elements is called once'
236 1 * mockCpsDataService.saveListElements('NCMP-Admin','ncmp-dmi-registry','/dmi-registry',_,null) >> {
238 assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
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)
257 def 'Delete schema set with an invalid schema set name'() {
258 when: 'the method to delete schema set is called with an invalid schema set name'
259 objectUnderTest.deleteSchemaSetWithCascade('invalid SchemaSet name')
260 then: 'a data validation exception is thrown'
261 thrown(DataValidationException)
262 and: 'the module service to delete schemaSet is not called'
263 0 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'sampleSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
266 def 'Query data nodes via cpsPath'() {
267 when: 'the method to query data nodes is called'
268 objectUnderTest.queryDataNodes('sample cpsPath')
269 then: 'the data persistence service method to query data nodes is invoked once'
270 1 * mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin','ncmp-dmi-registry','sample cpsPath', INCLUDE_ALL_DESCENDANTS)
273 def 'Get data node via xPath'() {
274 when: 'the method to get data nodes is called'
275 objectUnderTest.getDataNode('sample xPath')
276 then: 'the data persistence service method to get data node is invoked once'
277 1 * mockCpsDataPersistenceService.getDataNode('NCMP-Admin','ncmp-dmi-registry','sample xPath', INCLUDE_ALL_DESCENDANTS)
280 def 'Get cmHandle data node'() {
281 given: 'expected xPath to get cmHandle data node'
282 def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']';
283 when: 'the method to get data nodes is called'
284 objectUnderTest.getCmHandleDataNode('sample cmHandleId')
285 then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
286 1 * mockCpsDataPersistenceService.getDataNode('NCMP-Admin','ncmp-dmi-registry',expectedXPath, INCLUDE_ALL_DESCENDANTS)
289 def 'Query anchors'() {
290 when: 'the method to query anchors is called'
291 objectUnderTest.queryAnchors(['sample-module-name'])
292 then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
293 1 * mockCpsAdminPersistenceService.queryAnchors('NFP-Operational',['sample-module-name'])
296 def 'Get anchors'() {
297 when: 'the method to get anchors with no parameters is called'
298 objectUnderTest.getAnchors()
299 then: 'the admin persistence service method to query anchors is invoked once with a specific dataspace name'
300 1 * mockCpsAdminPersistenceService.getAnchors('NFP-Operational')
303 def 'Replace list content'() {
304 when: 'replace list content method is called with xpath and data nodes collection'
305 objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
306 then: 'the cps data service method to replace list content is invoked once with same parameters'
307 1 * mockCpsDataService.replaceListContent('NCMP-Admin', 'ncmp-dmi-registry',
308 'sample xpath', [new DataNode()], NO_TIMESTAMP);
311 def 'Delete data node via xPath'() {
312 when: 'Delete data node method is called with xpath as parameter'
313 objectUnderTest.deleteDataNode('sample dataNode xpath')
314 then: 'the cps data service method to delete data node is invoked once with the same xPath'
315 1 * mockCpsDataService.deleteDataNode('NCMP-Admin', 'ncmp-dmi-registry',
316 'sample dataNode xpath', NO_TIMESTAMP);