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.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
45 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
47 class InventoryPersistenceSpec 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 InventoryPersistence(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'])]
83 def static sampleDataNodes = [new DataNode()]
85 def "Retrieve CmHandle using datanode with #scenario."() {
86 given: 'the cps data service returns a data node from the DMI registry'
87 def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
88 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
89 when: 'retrieving the yang modelled cm handle'
90 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
91 then: 'the result has the correct id and service names'
92 result.id == cmHandleId
93 result.dmiServiceName == 'common service name'
94 result.dmiDataServiceName == 'data service name'
95 result.dmiModelServiceName == 'model service name'
96 and: 'the expected DMI properties'
97 result.dmiProperties == expectedDmiProperties
98 result.publicProperties == expectedPublicProperties
99 and: 'the state details are returned'
100 result.compositeState.cmHandleState == expectedCompositeState
101 where: 'the following parameters are used'
102 scenario | childDataNodes || expectedDmiProperties || expectedPublicProperties || expectedCompositeState
103 'no properties' | [] || [] || [] || null
104 'DMI and public properties' | childDataNodesForCmHandleWithAllProperties || [new YangModelCmHandle.Property("name1", "value1")] || [new YangModelCmHandle.Property("name2", "value2")] || null
105 'just DMI properties' | childDataNodesForCmHandleWithDMIProperties || [new YangModelCmHandle.Property("name1", "value1")] || [] || null
106 'just public properties' | childDataNodesForCmHandleWithPublicProperties || [] || [new YangModelCmHandle.Property("name2", "value2")] || null
107 'with state details' | childDataNodesForCmHandleWithState || [] || [] || CmHandleState.ADVISED
110 def "Retrieve CmHandle using datanode with invalid CmHandle id."() {
111 when: 'retrieving the yang modelled cm handle with an invalid id'
112 def result = objectUnderTest.getYangModelCmHandle('cm handle id with spaces')
113 then: 'a data validation exception is thrown'
114 thrown(DataValidationException)
115 and: 'the result is not returned'
119 def "Handling missing service names as null CPS-1043."() {
120 given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes'
121 def dataNode = new DataNode(childDataNodes:[], leaves: [:])
122 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
123 when: 'retrieving the yang modelled cm handle'
124 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
125 then: 'the service names ae returned as null'
126 result.dmiServiceName == null
127 result.dmiDataServiceName == null
128 result.dmiModelServiceName == null
131 def 'Get a Cm Handle Composite State'() {
132 given: 'a valid cm handle id'
133 def cmHandleId = 'Some-Cm-Handle'
134 def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
135 and: 'cps data service returns a valid data node'
136 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
137 '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
138 when: 'get cm handle state is invoked'
139 def result = objectUnderTest.getCmHandleState(cmHandleId)
140 then: 'result has returned the correct cm handle state'
141 result.cmHandleState == CmHandleState.ADVISED
144 def 'Update Cm Handle with #scenario State'() {
145 given: 'a cm handle and a composite state'
146 def cmHandleId = 'Some-Cm-Handle'
147 def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
148 when: 'update cm handle state is invoked with the #scenario state'
149 objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
150 then: 'update node leaves is invoked with the correct params'
151 1 * mockCpsDataService.replaceNodeTree('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
152 where: 'the following states are used'
153 scenario | cmHandleState || expectedJsonData
154 'READY' | CmHandleState.READY || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
155 'LOCKED' | CmHandleState.LOCKED || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
156 'DELETING' | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
159 def 'Get Cm Handles By State'() {
160 given: 'a cm handle state to query'
161 def cmHandleState = CmHandleState.ADVISED
162 and: 'cps data service returns a list of data nodes'
163 mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
164 '//state[@cm-handle-state="ADVISED"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
165 when: 'get cm handles by state is invoked'
166 def result = objectUnderTest.getCmHandlesByState(cmHandleState)
167 then: 'the returned result is a list of data nodes returned by cps data service'
168 assert result == sampleDataNodes
171 def 'Get Cm Handles By State and Cm-Handle Id'() {
172 given: 'a cm handle state to query'
173 def cmHandleState = CmHandleState.READY
174 and: 'cps data service returns a list of data nodes'
175 mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
176 '//cm-handles[@id=\'some-cm-handle\']/state[@cm-handle-state="'+ 'READY'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
177 when: 'get cm handles by state and id is invoked'
178 def result = objectUnderTest.getCmHandlesByIdAndState(cmHandleId, cmHandleState)
179 then: 'the returned result is a list of data nodes returned by cps data service'
180 assert result == sampleDataNodes
183 def 'Get Cm Handles By Operational Sync State : UNSYNCHRONIZED'() {
184 given: 'a cm handle state to query'
185 def cmHandleState = CmHandleState.READY
186 and: 'cps data service returns a list of data nodes'
187 mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
188 '//state/datastores/operational[@sync-state="'+'UNSYNCHRONIZED'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
189 when: 'get cm handles by operational sync state as UNSYNCHRONIZED is invoked'
190 def result = objectUnderTest.getCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED)
191 then: 'the returned result is a list of data nodes returned by cps data service'
192 assert result == sampleDataNodes
195 def 'Retrieve cm handle by cps path '() {
196 given: 'a cm handle state to query based on the cps path'
197 def cmHandleDataNode = new DataNode(xpath: 'xpath', leaves: ['cm-handle-state': 'LOCKED'])
198 def cpsPath = '//cps-path'
199 and: 'cps data service returns a valid data node'
200 mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
201 cpsPath, INCLUDE_ALL_DESCENDANTS)
202 >> Arrays.asList(cmHandleDataNode)
203 when: 'get cm handles by cps path is invoked'
204 def result = objectUnderTest.getCmHandleDataNodesByCpsPath(cpsPath, INCLUDE_ALL_DESCENDANTS)
205 then: 'the returned result is a list of data nodes returned by cps data service'
206 assert result.contains(cmHandleDataNode)
209 def 'Get module definitions'() {
210 given: 'cps module service returns a collection of module definitions'
211 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
212 mockCpsModuleService.getModuleDefinitionsByAnchorName('NFP-Operational','some-cmHandle-Id') >> moduleDefinitions
213 when: 'get module definitions by cmHandle is invoked'
214 def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
215 then: 'the returned result are the same module definitions as returned from the module service'
216 assert result == moduleDefinitions
219 def 'Get module references'() {
220 given: 'cps module service returns a collection of module references'
221 def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
222 mockCpsModuleService.getYangResourcesModuleReferences('NFP-Operational','some-cmHandle-Id') >> moduleReferences
223 when: 'get yang resources module references by cmHandle is invoked'
224 def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
225 then: 'the returned result is a collection of module definitions'
226 assert result == moduleReferences
229 def 'Save list elements'() {
230 when: 'the method to save list elements is called'
231 objectUnderTest.saveListElements('sample Json data')
232 then: 'the data service method to save list elements is called once'
233 1 * mockCpsDataService.saveListElements('NCMP-Admin','ncmp-dmi-registry','/dmi-registry','sample Json data',null)
236 def 'Delete list or list elements'() {
237 when: 'the method to delete list or list elements is called'
238 objectUnderTest.deleteListOrListElement('sample xPath')
239 then: 'the data service method to save list elements is called once'
240 1 * mockCpsDataService.deleteListOrListElement('NCMP-Admin','ncmp-dmi-registry','sample xPath',null)
243 def 'Delete schema set with a valid schema set name'() {
244 when: 'the method to delete schema set is called with valid schema set name'
245 objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
246 then: 'the module service to delete schemaSet is invoked once'
247 1 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
250 def 'Delete schema set with an invalid schema set name'() {
251 when: 'the method to delete schema set is called with an invalid schema set name'
252 objectUnderTest.deleteSchemaSetWithCascade('invalid SchemaSet name')
253 then: 'a data validation exception is thrown'
254 thrown(DataValidationException)
255 and: 'the module service to delete schemaSet is not called'
256 0 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'sampleSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
259 def 'Query data nodes via cpsPath'() {
260 when: 'the method to query data nodes is called'
261 objectUnderTest.queryDataNodes('sample cpsPath')
262 then: 'the data persistence service method to query data nodes is invoked once'
263 1 * mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin','ncmp-dmi-registry','sample cpsPath', INCLUDE_ALL_DESCENDANTS)
266 def 'Get data node via xPath'() {
267 when: 'the method to get data nodes is called'
268 objectUnderTest.getDataNode('sample xPath')
269 then: 'the data persistence service method to get data node is invoked once'
270 1 * mockCpsDataPersistenceService.getDataNode('NCMP-Admin','ncmp-dmi-registry','sample xPath', INCLUDE_ALL_DESCENDANTS)
273 def 'Query anchors'() {
274 when: 'the method to query anchors is called'
275 objectUnderTest.queryAnchors(['sample-module-name'])
276 then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
277 1 * mockCpsAdminPersistenceService.queryAnchors('NFP-Operational',['sample-module-name'])
280 def 'Get anchors'() {
281 when: 'the method to get anchors with no parameters is called'
282 objectUnderTest.getAnchors()
283 then: 'the admin persistence service method to query anchors is invoked once with a specific dataspace name'
284 1 * mockCpsAdminPersistenceService.getAnchors('NFP-Operational')