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'])]
83 def "Retrieve CmHandle using datanode with #scenario."() {
84 given: 'the cps data service returns a data node from the DMI registry'
85 def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
86 mockCpsDataPersistenceService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
87 when: 'retrieving the yang modelled cm handle'
88 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
89 then: 'the result has the correct id and service names'
90 result.id == cmHandleId
91 result.dmiServiceName == 'common service name'
92 result.dmiDataServiceName == 'data service name'
93 result.dmiModelServiceName == 'model service name'
94 and: 'the expected DMI properties'
95 result.dmiProperties == expectedDmiProperties
96 result.publicProperties == expectedPublicProperties
97 and: 'the state details are returned'
98 result.compositeState.cmHandleState == expectedCompositeState
99 where: 'the following parameters are used'
100 scenario | childDataNodes || expectedDmiProperties || expectedPublicProperties || expectedCompositeState
101 'no properties' | [] || [] || [] || null
102 'DMI and public properties' | childDataNodesForCmHandleWithAllProperties || [new YangModelCmHandle.Property("name1", "value1")] || [new YangModelCmHandle.Property("name2", "value2")] || null
103 'just DMI properties' | childDataNodesForCmHandleWithDMIProperties || [new YangModelCmHandle.Property("name1", "value1")] || [] || null
104 'just public properties' | childDataNodesForCmHandleWithPublicProperties || [] || [new YangModelCmHandle.Property("name2", "value2")] || null
105 'with state details' | childDataNodesForCmHandleWithState || [] || [] || CmHandleState.ADVISED
108 def "Retrieve CmHandle using datanode with invalid CmHandle id."() {
109 when: 'retrieving the yang modelled cm handle with an invalid id'
110 def result = objectUnderTest.getYangModelCmHandle('cm handle id with spaces')
111 then: 'a data validation exception is thrown'
112 thrown(DataValidationException)
113 and: 'the result is not returned'
117 def "Handling missing service names as null CPS-1043."() {
118 given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes'
119 def dataNode = new DataNode(childDataNodes:[], leaves: [:])
120 mockCpsDataPersistenceService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
121 when: 'retrieving the yang modelled cm handle'
122 def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
123 then: 'the service names ae returned as null'
124 result.dmiServiceName == null
125 result.dmiDataServiceName == null
126 result.dmiModelServiceName == null
129 def 'Get a Cm Handle Composite State'() {
130 given: 'a valid cm handle id'
131 def cmHandleId = 'Some-Cm-Handle'
132 def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
133 and: 'cps data service returns a valid data node'
134 mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
135 '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
136 when: 'get cm handle state is invoked'
137 def result = objectUnderTest.getCmHandleState(cmHandleId)
138 then: 'result has returned the correct cm handle state'
139 result.cmHandleState == CmHandleState.ADVISED
142 def 'Update Cm Handle with #scenario State'() {
143 given: 'a cm handle and a composite state'
144 def cmHandleId = 'Some-Cm-Handle'
145 def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
146 when: 'update cm handle state is invoked with the #scenario state'
147 objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
148 then: 'update node leaves is invoked with the correct params'
149 1 * mockCpsDataService.replaceNodeTree('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
150 where: 'the following states are used'
151 scenario | cmHandleState || expectedJsonData
152 'READY' | CmHandleState.READY || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
153 'LOCKED' | CmHandleState.LOCKED || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
154 'DELETING' | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
157 def 'Get module definitions'() {
158 given: 'cps module service returns a collection of module definitions'
159 def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
160 mockCpsModuleService.getModuleDefinitionsByAnchorName('NFP-Operational','some-cmHandle-Id') >> moduleDefinitions
161 when: 'get module definitions by cmHandle is invoked'
162 def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
163 then: 'the returned result are the same module definitions as returned from the module service'
164 assert result == moduleDefinitions
167 def 'Get module references'() {
168 given: 'cps module service returns a collection of module references'
169 def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
170 mockCpsModuleService.getYangResourcesModuleReferences('NFP-Operational','some-cmHandle-Id') >> moduleReferences
171 when: 'get yang resources module references by cmHandle is invoked'
172 def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
173 then: 'the returned result is a collection of module definitions'
174 assert result == moduleReferences
177 def 'Save Cmhandle'() {
178 given: 'cmHandle represented as Yang Model'
179 def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: [])
180 when: 'the method to save cmhandle is called'
181 objectUnderTest.saveCmHandle(yangModelCmHandle)
182 then: 'the data service method to save list elements is called once'
183 1 * mockCpsDataService.saveListElements('NCMP-Admin','ncmp-dmi-registry','/dmi-registry',_,null) >> {
185 assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
190 def 'Delete list or list elements'() {
191 when: 'the method to delete list or list elements is called'
192 objectUnderTest.deleteListOrListElement('sample xPath')
193 then: 'the data service method to save list elements is called once'
194 1 * mockCpsDataService.deleteListOrListElement('NCMP-Admin','ncmp-dmi-registry','sample xPath',null)
197 def 'Delete schema set with a valid schema set name'() {
198 when: 'the method to delete schema set is called with valid schema set name'
199 objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
200 then: 'the module service to delete schemaSet is invoked once'
201 1 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
204 def 'Delete schema set with an invalid schema set name'() {
205 when: 'the method to delete schema set is called with an invalid schema set name'
206 objectUnderTest.deleteSchemaSetWithCascade('invalid SchemaSet name')
207 then: 'a data validation exception is thrown'
208 thrown(DataValidationException)
209 and: 'the module service to delete schemaSet is not called'
210 0 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'sampleSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
213 def 'Get data node via xPath'() {
214 when: 'the method to get data nodes is called'
215 objectUnderTest.getDataNode('sample xPath')
216 then: 'the data persistence service method to get data node is invoked once'
217 1 * mockCpsDataPersistenceService.getDataNode('NCMP-Admin','ncmp-dmi-registry','sample xPath', INCLUDE_ALL_DESCENDANTS)
220 def 'Get cmHandle data node'() {
221 given: 'expected xPath to get cmHandle data node'
222 def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']';
223 when: 'the method to get data nodes is called'
224 objectUnderTest.getCmHandleDataNode('sample cmHandleId')
225 then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
226 1 * mockCpsDataPersistenceService.getDataNode('NCMP-Admin','ncmp-dmi-registry',expectedXPath, INCLUDE_ALL_DESCENDANTS)
229 def 'Query anchors'() {
230 when: 'the method to query anchors is called'
231 objectUnderTest.queryAnchors(['sample-module-name'])
232 then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
233 1 * mockCpsAdminPersistenceService.queryAnchors('NFP-Operational',['sample-module-name'])
236 def 'Get anchors'() {
237 when: 'the method to get anchors with no parameters is called'
238 objectUnderTest.getAnchors()
239 then: 'the admin persistence service method to query anchors is invoked once with a specific dataspace name'
240 1 * mockCpsAdminPersistenceService.getAnchors('NFP-Operational')
243 def 'Replace list content'() {
244 when: 'replace list content method is called with xpath and data nodes collection'
245 objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
246 then: 'the cps data service method to replace list content is invoked once with same parameters'
247 1 * mockCpsDataService.replaceListContent('NCMP-Admin', 'ncmp-dmi-registry',
248 'sample xpath', [new DataNode()], NO_TIMESTAMP);
251 def 'Delete data node via xPath'() {
252 when: 'Delete data node method is called with xpath as parameter'
253 objectUnderTest.deleteDataNode('sample dataNode xpath')
254 then: 'the cps data service method to delete data node is invoked once with the same xPath'
255 1 * mockCpsDataService.deleteDataNode('NCMP-Admin', 'ncmp-dmi-registry',
256 'sample dataNode xpath', NO_TIMESTAMP);