Use getDataNodes (plural version) into NCMP to get CM handles
[cps.git] / cps-ncmp-service / src / test / groovy / org / onap / cps / ncmp / api / inventory / InventoryPersistenceImplSpec.groovy
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2022-2023 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.CpsAdminService
26 import org.onap.cps.api.CpsDataService
27 import org.onap.cps.api.CpsModuleService
28 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
29 import org.onap.cps.spi.CascadeDeleteAllowed
30 import org.onap.cps.spi.FetchDescendantsOption
31 import org.onap.cps.spi.exceptions.DataValidationException
32 import org.onap.cps.spi.model.DataNode
33 import org.onap.cps.spi.model.ModuleDefinition
34 import org.onap.cps.spi.model.ModuleReference
35 import org.onap.cps.utils.JsonObjectMapper
36 import org.onap.cps.spi.utils.CpsValidator
37 import spock.lang.Shared
38 import spock.lang.Specification
39 import java.time.OffsetDateTime
40 import java.time.ZoneOffset
41 import java.time.format.DateTimeFormatter
42 import java.util.stream.Collectors
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 mockCpsAdminService = Mock(CpsAdminService)
56
57     def mockCpsValidator = Mock(CpsValidator)
58
59     def objectUnderTest = new InventoryPersistenceImpl(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService,
60             mockCpsAdminService, mockCpsValidator)
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     def cmHandleId2 = 'another-cm-handle'
70     def xpath2 = "/dmi-registry/cm-handles[@id='another-cm-handle']"
71
72     @Shared
73     def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"]),
74                                                       new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
75
76     @Shared
77     def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])]
78
79     @Shared
80     def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
81
82     @Shared
83     def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
84
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         and: 'the CM Handle ID is validated'
102             1 * mockCpsValidator.validateNameCharacters(cmHandleId)
103         where: 'the following parameters are used'
104             scenario                    | childDataNodes                                || expectedDmiProperties                               || expectedPublicProperties                              || expectedCompositeState
105             'no properties'             | []                                            || []                                                  || []                                                    || null
106             'DMI and public properties' | childDataNodesForCmHandleWithAllProperties    || [new YangModelCmHandle.Property("name1", "value1")] || [new YangModelCmHandle.Property("name2", "value2")]   || null
107             'just DMI properties'       | childDataNodesForCmHandleWithDMIProperties    || [new YangModelCmHandle.Property("name1", "value1")] || []                                                    || null
108             'just public properties'    | childDataNodesForCmHandleWithPublicProperties || []                                                  || [new YangModelCmHandle.Property("name2", "value2")]   || null
109             'with state details'        | childDataNodesForCmHandleWithState            || []                                                  || []                                                    || CmHandleState.ADVISED
110     }
111
112     def "Handling missing service names as null."() {
113         given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes'
114             def dataNode = new DataNode(childDataNodes:[], leaves: [:])
115             mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
116         when: 'retrieving the yang modelled cm handle'
117             def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
118         then: 'the service names are returned as null'
119             result.dmiServiceName == null
120             result.dmiDataServiceName == null
121             result.dmiModelServiceName == null
122         and: 'the CM Handle ID is validated'
123             1 * mockCpsValidator.validateNameCharacters(cmHandleId)
124     }
125
126     def "Retrieve multiple YangModelCmHandles"() {
127         given: 'the cps data service returns 2 data nodes from the DMI registry'
128             def dataNodes = [new DataNode(xpath: xpath), new DataNode(xpath: xpath2)]
129             mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry', [xpath, xpath2] , INCLUDE_ALL_DESCENDANTS) >> dataNodes
130         when: 'retrieving the yang modelled cm handle'
131             def results = objectUnderTest.getYangModelCmHandles([cmHandleId, cmHandleId2])
132         then: 'verify both have returned and cmhandleIds are correct'
133             assert results.size() == 2
134             assert results.id.containsAll([cmHandleId, cmHandleId2])
135     }
136
137     def "Handling name validation errors in getYangModelCmHandles."() {
138         given: 'the cps data service returns one of two data nodes from the DMI registry with empty leaf attributes'
139             mockCpsValidator.validateNameCharacters(cmHandleId) >> {throw new DataValidationException('some message', 'some detail')}
140         when:
141             objectUnderTest.getYangModelCmHandle(cmHandleId)
142         then: 'exception is thrown'
143             thrown(DataValidationException)
144     }
145
146     def 'Get a Cm Handle Composite State'() {
147         given: 'a valid cm handle id'
148             def cmHandleId = 'Some-Cm-Handle'
149             def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
150         and: 'cps data service returns a valid data node'
151             mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
152                     '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
153         when: 'get cm handle state is invoked'
154             def result = objectUnderTest.getCmHandleState(cmHandleId)
155         then: 'result has returned the correct cm handle state'
156             result.cmHandleState == CmHandleState.ADVISED
157         and: 'the CM Handle ID is validated'
158             1 * mockCpsValidator.validateNameCharacters(cmHandleId)
159     }
160
161     def 'Update Cm Handle with #scenario State'() {
162         given: 'a cm handle and a composite state'
163             def cmHandleId = 'Some-Cm-Handle'
164             def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
165         when: 'update cm handle state is invoked with the #scenario state'
166             objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
167         then: 'update node leaves is invoked with the correct params'
168             1 * mockCpsDataService.updateDataNodeAndDescendants('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
169         where: 'the following states are used'
170             scenario    | cmHandleState          || expectedJsonData
171             'READY'     | CmHandleState.READY    || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
172             'LOCKED'    | CmHandleState.LOCKED   || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
173             'DELETING'  | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
174     }
175
176     def 'Update Cm Handles with #scenario States'() {
177         given: 'a map of cm handles composite states'
178             def compositeState1 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
179             def compositeState2 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
180         when: 'update cm handle state is invoked with the #scenario state'
181             def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2]
182             objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
183         then: 'update node leaves is invoked with the correct params'
184             1 * mockCpsDataService.updateDataNodesAndDescendants('NCMP-Admin', 'ncmp-dmi-registry', cmHandlesJsonDataMap, _ as OffsetDateTime)
185         where: 'the following states are used'
186             scenario    | cmHandleState          || cmHandlesJsonDataMap
187             '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"}}']
188             '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"}}']
189             '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"}}']
190     }
191
192     def 'Get module definitions'() {
193         given: 'cps module service returns a collection of module definitions'
194             def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
195             mockCpsModuleService.getModuleDefinitionsByAnchorName('NFP-Operational','some-cmHandle-Id') >> moduleDefinitions
196         when: 'get module definitions by cmHandle is invoked'
197             def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
198         then: 'the returned result are the same module definitions as returned from the module service'
199             assert result == moduleDefinitions
200     }
201
202     def 'Get module references'() {
203         given: 'cps module service returns a collection of module references'
204             def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
205             mockCpsModuleService.getYangResourcesModuleReferences('NFP-Operational','some-cmHandle-Id') >> moduleReferences
206         when: 'get yang resources module references by cmHandle is invoked'
207             def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
208         then: 'the returned result is a collection of module definitions'
209             assert result == moduleReferences
210         and: 'the CM Handle ID is validated'
211             1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id')
212     }
213
214     def 'Save Cmhandle'() {
215         given: 'cmHandle represented as Yang Model'
216             def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: [])
217         when: 'the method to save cmhandle is called'
218             objectUnderTest.saveCmHandle(yangModelCmHandle)
219         then: 'the data service method to save list elements is called once'
220             1 * mockCpsDataService.saveListElements('NCMP-Admin','ncmp-dmi-registry','/dmi-registry',_,null) >> {
221                 args -> {
222                     assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
223                 }
224             }
225     }
226
227     def 'Save Multiple Cmhandles'() {
228         given: 'cm handles represented as Yang Model'
229             def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1')
230             def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2')
231         when: 'the cm handles are saved'
232             objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2])
233         then: 'CPS Data Service persists both cm handles as a batch'
234             1 * mockCpsDataService.saveListElementsBatch('NCMP-Admin','ncmp-dmi-registry','/dmi-registry',_,null) >> {
235                 args -> {
236                     def jsonDataList = (args[3] as List)
237                     (jsonDataList[0] as String).contains('cmhandle1')
238                     (jsonDataList[0] as String).contains('cmhandle2')
239                 }
240             }
241     }
242
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)
248     }
249
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)
255         and: 'the CM Handle ID is validated'
256             1 * mockCpsValidator.validateNameCharacters('validSchemaSetName')
257     }
258
259     def 'Get data node via xPath'() {
260         when: 'the method to get data nodes is called'
261             objectUnderTest.getDataNode('sample xPath')
262         then: 'the data persistence service method to get data node is invoked once'
263             1 * mockCpsDataService.getDataNode('NCMP-Admin','ncmp-dmi-registry','sample xPath', INCLUDE_ALL_DESCENDANTS)
264     }
265
266     def 'Get cmHandle data node'() {
267         given: 'expected xPath to get cmHandle data node'
268             def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']';
269         when: 'the method to get data nodes is called'
270             objectUnderTest.getCmHandleDataNode('sample cmHandleId')
271         then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
272             1 * mockCpsDataService.getDataNode('NCMP-Admin','ncmp-dmi-registry',expectedXPath, INCLUDE_ALL_DESCENDANTS)
273     }
274
275     def 'Get CM handles that has given module names'() {
276         when: 'the method to get cm handles is called'
277             objectUnderTest.getCmHandleIdsWithGivenModules(['sample-module-name'])
278         then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
279             1 * mockCpsAdminService.queryAnchorNames('NFP-Operational',['sample-module-name'])
280     }
281
282     def 'Replace list content'() {
283         when: 'replace list content method is called with xpath and data nodes collection'
284             objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
285         then: 'the cps data service method to replace list content is invoked once with same parameters'
286             1 * mockCpsDataService.replaceListContent('NCMP-Admin', 'ncmp-dmi-registry',
287                     'sample xpath', [new DataNode()], NO_TIMESTAMP);
288     }
289
290     def 'Delete data node via xPath'() {
291         when: 'Delete data node method is called with xpath as parameter'
292             objectUnderTest.deleteDataNode('sample dataNode xpath')
293         then: 'the cps data service method to delete data node is invoked once with the same xPath'
294             1 * mockCpsDataService.deleteDataNode('NCMP-Admin', 'ncmp-dmi-registry',
295                     'sample dataNode xpath', NO_TIMESTAMP);
296     }
297
298     def 'Delete multiple data nodes via xPath'() {
299         when: 'Delete data nodes method is called with multiple xpaths as parameters'
300             objectUnderTest.deleteDataNodes(['xpath1', 'xpath2'])
301         then: 'the cps data service method to delete data nodes is invoked once with the same xPaths'
302             1 * mockCpsDataService.deleteDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
303                 ['xpath1', 'xpath2'], NO_TIMESTAMP);
304     }
305
306 }