0d459fd0facde4b5259783ae4794b560955486b4
[cps.git] / cps-ncmp-service / src / test / groovy / org / onap / cps / ncmp / api / inventory / InventoryPersistenceImplSpec.groovy
1 /*
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
9  *
10  *        http://www.apache.org/licenses/LICENSE-2.0
11  *
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.
17  *
18  *  SPDX-License-Identifier: Apache-2.0
19  *  ============LICENSE_END=========================================================
20  */
21
22 package org.onap.cps.ncmp.api.inventory
23
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
39
40 import java.time.OffsetDateTime
41 import java.time.ZoneOffset
42 import java.time.format.DateTimeFormatter
43
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
47 class InventoryPersistenceImplSpec extends Specification {
48
49     def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
50
51     def mockCpsDataService = Mock(CpsDataService)
52
53     def mockCpsModuleService = Mock(CpsModuleService)
54
55     def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService)
56
57     def mockCpsAdminPersistenceService = Mock(CpsAdminPersistenceService)
58
59     def objectUnderTest = new InventoryPersistenceImpl(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService,
60             mockCpsDataPersistenceService, mockCpsAdminPersistenceService)
61
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))
64
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']"
68
69     @Shared
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"])]
72
73     @Shared
74     def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])]
75
76     @Shared
77     def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
78
79     @Shared
80     def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
81
82     def "Retrieve CmHandle using datanode with #scenario."() {
83         given: 'the cps data service returns a data node from the DMI registry'
84             def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
85             mockCpsDataPersistenceService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
86         when: 'retrieving the yang modelled cm handle'
87             def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
88         then: 'the result has the correct id and service names'
89             result.id == cmHandleId
90             result.dmiServiceName == 'common service name'
91             result.dmiDataServiceName == 'data service name'
92             result.dmiModelServiceName == 'model service name'
93         and: 'the expected DMI properties'
94             result.dmiProperties == expectedDmiProperties
95             result.publicProperties == expectedPublicProperties
96         and: 'the state details are returned'
97             result.compositeState.cmHandleState == expectedCompositeState
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
105     }
106
107     def "Retrieve CmHandle using datanode with invalid CmHandle id."() {
108         when: 'retrieving the yang modelled cm handle with an invalid id'
109             def result = objectUnderTest.getYangModelCmHandle('cm handle id with spaces')
110         then: 'a data validation exception is thrown'
111             thrown(DataValidationException)
112         and: 'the result is not returned'
113             result == null
114     }
115
116     def "Handling missing service names as null CPS-1043."() {
117         given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes'
118             def dataNode = new DataNode(childDataNodes:[], leaves: [:])
119             mockCpsDataPersistenceService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
120         when: 'retrieving the yang modelled cm handle'
121             def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
122         then: 'the service names ae returned as null'
123             result.dmiServiceName == null
124             result.dmiDataServiceName == null
125             result.dmiModelServiceName == null
126     }
127
128     def 'Get a Cm Handle Composite State'() {
129         given: 'a valid cm handle id'
130             def cmHandleId = 'Some-Cm-Handle'
131             def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
132         and: 'cps data service returns a valid data node'
133             mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
134                     '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
135         when: 'get cm handle state is invoked'
136             def result = objectUnderTest.getCmHandleState(cmHandleId)
137         then: 'result has returned the correct cm handle state'
138             result.cmHandleState == CmHandleState.ADVISED
139     }
140
141     def 'Update Cm Handle with #scenario State'() {
142         given: 'a cm handle and a composite state'
143             def cmHandleId = 'Some-Cm-Handle'
144             def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
145         when: 'update cm handle state is invoked with the #scenario state'
146             objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
147         then: 'update node leaves is invoked with the correct params'
148             1 * mockCpsDataService.updateDataNodeAndDescendants('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
149         where: 'the following states are used'
150             scenario    | cmHandleState          || expectedJsonData
151             'READY'     | CmHandleState.READY    || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
152             'LOCKED'    | CmHandleState.LOCKED   || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
153             'DELETING'  | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
154     }
155
156     def 'Update Cm Handles with #scenario States'() {
157         given: 'a map of cm handles composite states'
158             def compositeState1 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
159             def compositeState2 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
160         when: 'update cm handle state is invoked with the #scenario state'
161             def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2]
162             objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
163         then: 'update node leaves is invoked with the correct params'
164             1 * mockCpsDataService.updateDataNodesAndDescendants('NCMP-Admin', 'ncmp-dmi-registry', cmHandlesJsonDataMap, _ as OffsetDateTime)
165         where: 'the following states are used'
166             scenario    | cmHandleState          || cmHandlesJsonDataMap
167             '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"}}']
168             '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"}}']
169             '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"}}']
170     }
171
172     def 'Get module definitions'() {
173         given: 'cps module service returns a collection of module definitions'
174             def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
175             mockCpsModuleService.getModuleDefinitionsByAnchorName('NFP-Operational','some-cmHandle-Id') >> moduleDefinitions
176         when: 'get module definitions by cmHandle is invoked'
177             def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
178         then: 'the returned result are the same module definitions as returned from the module service'
179             assert result == moduleDefinitions
180     }
181
182     def 'Get module references'() {
183         given: 'cps module service returns a collection of module references'
184             def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
185             mockCpsModuleService.getYangResourcesModuleReferences('NFP-Operational','some-cmHandle-Id') >> moduleReferences
186         when: 'get yang resources module references by cmHandle is invoked'
187             def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
188         then: 'the returned result is a collection of module definitions'
189             assert result == moduleReferences
190     }
191
192     def 'Save Cmhandle'() {
193         given: 'cmHandle represented as Yang Model'
194             def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: [])
195         when: 'the method to save cmhandle is called'
196             objectUnderTest.saveCmHandle(yangModelCmHandle)
197         then: 'the data service method to save list elements is called once'
198             1 * mockCpsDataService.saveListElements('NCMP-Admin','ncmp-dmi-registry','/dmi-registry',_,null) >> {
199                 args -> {
200                     assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
201                 }
202             }
203     }
204
205     def 'Save Multiple Cmhandles'() {
206         given: 'cm handles represented as Yang Model'
207             def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1')
208             def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2')
209         when: 'the cm handles are saved'
210             objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2])
211         then: 'CPS Data Service persists both cm handles as a batch'
212             1 * mockCpsDataService.saveListElementsBatch('NCMP-Admin','ncmp-dmi-registry','/dmi-registry',_,null) >> {
213                 args -> {
214                     def jsonDataList = (args[3] as List)
215                     (jsonDataList[0] as String).contains('cmhandle1')
216                     (jsonDataList[0] as String).contains('cmhandle2')
217                 }
218             }
219     }
220
221     def 'Delete list or list elements'() {
222         when: 'the method to delete list or list elements is called'
223             objectUnderTest.deleteListOrListElement('sample xPath')
224         then: 'the data service method to save list elements is called once'
225             1 * mockCpsDataService.deleteListOrListElement('NCMP-Admin','ncmp-dmi-registry','sample xPath',null)
226     }
227
228     def 'Delete schema set with a valid schema set name'() {
229         when: 'the method to delete schema set is called with valid schema set name'
230             objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
231         then: 'the module service to delete schemaSet is invoked once'
232             1 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
233     }
234
235     def 'Delete schema set with an invalid schema set name'() {
236         when: 'the method to delete schema set is called with an invalid schema set name'
237             objectUnderTest.deleteSchemaSetWithCascade('invalid SchemaSet name')
238         then: 'a data validation exception is thrown'
239             thrown(DataValidationException)
240         and: 'the module service to delete schemaSet is not called'
241             0 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'sampleSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
242     }
243
244     def 'Get data node via xPath'() {
245         when: 'the method to get data nodes is called'
246             objectUnderTest.getDataNode('sample xPath')
247         then: 'the data persistence service method to get data node is invoked once'
248             1 * mockCpsDataPersistenceService.getDataNode('NCMP-Admin','ncmp-dmi-registry','sample xPath', INCLUDE_ALL_DESCENDANTS)
249     }
250
251     def 'Get cmHandle data node'() {
252         given: 'expected xPath to get cmHandle data node'
253             def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']';
254         when: 'the method to get data nodes is called'
255             objectUnderTest.getCmHandleDataNode('sample cmHandleId')
256         then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
257             1 * mockCpsDataPersistenceService.getDataNode('NCMP-Admin','ncmp-dmi-registry',expectedXPath, INCLUDE_ALL_DESCENDANTS)
258     }
259
260     def 'Query anchors'() {
261         when: 'the method to query anchors is called'
262             objectUnderTest.queryAnchors(['sample-module-name'])
263         then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
264             1 * mockCpsAdminPersistenceService.queryAnchors('NFP-Operational',['sample-module-name'])
265     }
266
267     def 'Get anchors'() {
268         when: 'the method to get anchors with no parameters is called'
269             objectUnderTest.getAnchors()
270         then: 'the admin persistence service method to query anchors is invoked once with a specific dataspace name'
271             1 * mockCpsAdminPersistenceService.getAnchors('NFP-Operational')
272     }
273
274     def 'Replace list content'() {
275         when: 'replace list content method is called with xpath and data nodes collection'
276             objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
277         then: 'the cps data service method to replace list content is invoked once with same parameters'
278             1 * mockCpsDataService.replaceListContent('NCMP-Admin', 'ncmp-dmi-registry',
279                     'sample xpath', [new DataNode()], NO_TIMESTAMP);
280     }
281
282     def 'Delete data node via xPath'() {
283         when: 'Delete data node method is called with xpath as parameter'
284             objectUnderTest.deleteDataNode('sample dataNode xpath')
285         then: 'the cps data service method to delete data node is invoked once with the same xPath'
286             1 * mockCpsDataService.deleteDataNode('NCMP-Admin', 'ncmp-dmi-registry',
287                     'sample dataNode xpath', NO_TIMESTAMP);
288     }
289
290 }