d58a1dfa06e6bb062cc9ed62fab638afb4a6fc20
[cps.git] /
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2022-2024 Nordix Foundation
4  *  Modifications Copyright (C) 2022 Bell Canada
5  *  Modifications Copyright (C) 2023 TechMahindra Ltd.
6  *  ================================================================================
7  *  Licensed under the Apache License, Version 2.0 (the "License");
8  *  you may not use this file except in compliance with the License.
9  *  You may obtain a copy of the License at
10  *
11  *        http://www.apache.org/licenses/LICENSE-2.0
12  *
13  *  Unless required by applicable law or agreed to in writing, software
14  *  distributed under the License is distributed on an "AS IS" BASIS,
15  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  *  See the License for the specific language governing permissions and
17  *  limitations under the License.
18  *
19  *  SPDX-License-Identifier: Apache-2.0
20  *  ============LICENSE_END=========================================================
21  */
22
23 package org.onap.cps.ncmp.impl.inventory
24
25 import com.fasterxml.jackson.databind.ObjectMapper
26 import org.onap.cps.api.CpsAnchorService
27 import org.onap.cps.api.CpsDataService
28 import org.onap.cps.api.CpsModuleService
29 import org.onap.cps.ncmp.api.inventory.models.CompositeState
30 import org.onap.cps.ncmp.impl.inventory.models.CmHandleState
31 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
32 import org.onap.cps.spi.CascadeDeleteAllowed
33 import org.onap.cps.spi.FetchDescendantsOption
34 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
35 import org.onap.cps.spi.model.DataNode
36 import org.onap.cps.spi.model.ModuleDefinition
37 import org.onap.cps.spi.model.ModuleReference
38 import org.onap.cps.spi.utils.CpsValidator
39 import org.onap.cps.utils.JsonObjectMapper
40 import spock.lang.Shared
41 import spock.lang.Specification
42
43 import java.time.OffsetDateTime
44 import java.time.ZoneOffset
45 import java.time.format.DateTimeFormatter
46
47 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DATASPACE_NAME
48 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
49 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT
50 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
51 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NO_TIMESTAMP
52 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
53 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
54
55 class InventoryPersistenceImplSpec extends Specification {
56
57     def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
58
59     def mockCpsDataService = Mock(CpsDataService)
60
61     def mockCpsModuleService = Mock(CpsModuleService)
62
63     def mockCpsAnchorService = Mock(CpsAnchorService)
64
65     def mockCpsValidator = Mock(CpsValidator)
66
67     def mockCmHandleQueries = Mock(CmHandleQueryService)
68
69     def objectUnderTest = new InventoryPersistenceImpl(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService,
70             mockCpsValidator, mockCpsAnchorService, mockCmHandleQueries)
71
72     def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
73             .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
74
75     def cmHandleId = 'some-cm-handle'
76     def leaves = ["id":cmHandleId,"dmi-service-name":"common service name","dmi-data-service-name":"data service name","dmi-model-service-name":"model service name"]
77     def xpath = "/dmi-registry/cm-handles[@id='some-cm-handle']"
78
79     def cmHandleId2 = 'another-cm-handle'
80     def xpath2 = "/dmi-registry/cm-handles[@id='another-cm-handle']"
81
82     @Shared
83     def childDataNodesForCmHandleWithAllProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"]),
84                                                       new DataNode(xpath: "/dmi-registry/cm-handles[@id='some cm handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
85
86     @Shared
87     def childDataNodesForCmHandleWithDMIProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/additional-properties[@name='name1']", leaves: ["name":"name1", "value":"value1"])]
88
89     @Shared
90     def childDataNodesForCmHandleWithPublicProperties = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/public-properties[@name='name2']", leaves: ["name":"name2","value":"value2"])]
91
92     @Shared
93     def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
94
95     def "Retrieve CmHandle using datanode with #scenario."() {
96         given: 'the cps data service returns a data node from the DMI registry'
97             def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
98             mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
99         when: 'retrieving the yang modelled cm handle'
100             def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
101         then: 'the result has the correct id and service names'
102             result.id == cmHandleId
103             result.dmiServiceName == 'common service name'
104             result.dmiDataServiceName == 'data service name'
105             result.dmiModelServiceName == 'model service name'
106         and: 'the expected DMI properties'
107             result.dmiProperties == expectedDmiProperties
108             result.publicProperties == expectedPublicProperties
109         and: 'the state details are returned'
110             result.compositeState.cmHandleState == expectedCompositeState
111         and: 'the CM Handle ID is validated'
112             1 * mockCpsValidator.validateNameCharacters(cmHandleId)
113         where: 'the following parameters are used'
114             scenario                    | childDataNodes                                || expectedDmiProperties                               || expectedPublicProperties                              || expectedCompositeState
115             'no properties'             | []                                            || []                                                  || []                                                    || null
116             'DMI and public properties' | childDataNodesForCmHandleWithAllProperties    || [new YangModelCmHandle.Property("name1", "value1")] || [new YangModelCmHandle.Property("name2", "value2")]   || null
117             'just DMI properties'       | childDataNodesForCmHandleWithDMIProperties    || [new YangModelCmHandle.Property("name1", "value1")] || []                                                    || null
118             'just public properties'    | childDataNodesForCmHandleWithPublicProperties || []                                                  || [new YangModelCmHandle.Property("name2", "value2")]   || null
119             'with state details'        | childDataNodesForCmHandleWithState            || []                                                  || []                                                    || CmHandleState.ADVISED
120     }
121
122     def "Handling missing service names as null."() {
123         given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes'
124             def dataNode = new DataNode(childDataNodes:[], leaves: ['id':cmHandleId])
125             mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS) >> [dataNode]
126         when: 'retrieving the yang modelled cm handle'
127             def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
128         then: 'the service names are returned as null'
129             result.dmiServiceName == null
130             result.dmiDataServiceName == null
131             result.dmiModelServiceName == null
132         and: 'the CM Handle ID is validated'
133             1 * mockCpsValidator.validateNameCharacters(cmHandleId)
134     }
135
136     def "Retrieve multiple YangModelCmHandles"() {
137         given: 'the cps data service returns 2 data nodes from the DMI registry'
138             def dataNodes = [new DataNode(xpath: xpath, leaves: ['id': cmHandleId]), new DataNode(xpath: xpath2, leaves: ['id': cmHandleId2])]
139             mockCpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, [xpath, xpath2] , INCLUDE_ALL_DESCENDANTS) >> dataNodes
140         when: 'retrieving the yang modelled cm handle'
141             def results = objectUnderTest.getYangModelCmHandles([cmHandleId, cmHandleId2])
142         then: 'verify both have returned and cmhandleIds are correct'
143             assert results.size() == 2
144             assert results.id.containsAll([cmHandleId, cmHandleId2])
145     }
146
147     def 'Get a Cm Handle Composite State'() {
148         given: 'a valid cm handle id'
149             def cmHandleId = 'Some-Cm-Handle'
150             def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
151         and: 'cps data service returns a valid data node'
152             mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
153                     '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode]
154         when: 'get cm handle state is invoked'
155             def result = objectUnderTest.getCmHandleState(cmHandleId)
156         then: 'result has returned the correct cm handle state'
157             result.cmHandleState == CmHandleState.ADVISED
158         and: 'the CM Handle ID is validated'
159             1 * mockCpsValidator.validateNameCharacters(cmHandleId)
160     }
161
162     def 'Update Cm Handle with #scenario State'() {
163         given: 'a cm handle and a composite state'
164             def cmHandleId = 'Some-Cm-Handle'
165             def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
166         when: 'update cm handle state is invoked with the #scenario state'
167             objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
168         then: 'update node leaves is invoked with the correct params'
169             1 * mockCpsDataService.updateDataNodeAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
170         where: 'the following states are used'
171             scenario    | cmHandleState          || expectedJsonData
172             'READY'     | CmHandleState.READY    || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
173             'LOCKED'    | CmHandleState.LOCKED   || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
174             'DELETING'  | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
175     }
176
177     def 'Update Cm Handles with #scenario States'() {
178         given: 'a map of cm handles composite states'
179             def compositeState1 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
180             def compositeState2 = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
181         when: 'update cm handle state is invoked with the #scenario state'
182             def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2]
183             objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
184         then: 'update node leaves is invoked with the correct params'
185             1 * mockCpsDataService.updateDataNodesAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandlesJsonDataMap, _ as OffsetDateTime)
186         where: 'the following states are used'
187             scenario    | cmHandleState          || cmHandlesJsonDataMap
188             '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"}}']
189             '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"}}']
190             '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"}}']
191     }
192
193     def 'Getting module definitions by module'() {
194         given: 'cps module service returns module definition for module name'
195             def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
196             mockCpsModuleService.getModuleDefinitionsByAnchorAndModule(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id', 'some-module', '2024-01-25') >> moduleDefinitions
197         when: 'get module definitions is invoked with module name'
198             def result = objectUnderTest.getModuleDefinitionsByCmHandleAndModule('some-cmHandle-Id', 'some-module', '2024-01-25')
199         then: 'returned result are the same module definitions as returned from module service'
200             assert result == moduleDefinitions
201         and: 'cm handle id and module name validated'
202             1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id', 'some-module')
203     }
204
205     def 'Getting module definitions with cm handle id'() {
206         given: 'cps module service returns module definitions for cm handle id'
207             def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
208             mockCpsModuleService.getModuleDefinitionsByAnchorName(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleDefinitions
209         when: 'get module definitions is invoked with cm handle id'
210             def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
211         then: 'the returned result are the same module definitions as returned from the module service'
212             assert result == moduleDefinitions
213     }
214
215     def 'Get module references'() {
216         given: 'cps module service returns a collection of module references'
217             def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
218             mockCpsModuleService.getYangResourcesModuleReferences(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME,'some-cmHandle-Id') >> moduleReferences
219         when: 'get yang resources module references by cmHandle is invoked'
220             def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
221         then: 'the returned result is a collection of module definitions'
222             assert result == moduleReferences
223         and: 'the CM Handle ID is validated'
224             1 * mockCpsValidator.validateNameCharacters('some-cmHandle-Id')
225     }
226
227     def 'Save Cmhandle'() {
228         given: 'cmHandle represented as Yang Model'
229             def yangModelCmHandle = new YangModelCmHandle(id: 'cmhandle', dmiProperties: [], publicProperties: [])
230         when: 'the method to save cmhandle is called'
231             objectUnderTest.saveCmHandle(yangModelCmHandle)
232         then: 'the data service method to save list elements is called once'
233             1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT,
234                     _,null) >> {
235                 args -> {
236                     assert args[3].startsWith('{"cm-handles":[{"id":"cmhandle","additional-properties":[],"public-properties":[]}]}')
237                 }
238             }
239     }
240
241     def 'Save Multiple Cmhandles'() {
242         given: 'cm handles represented as Yang Model'
243             def yangModelCmHandle1 = new YangModelCmHandle(id: 'cmhandle1')
244             def yangModelCmHandle2 = new YangModelCmHandle(id: 'cmhandle2')
245         when: 'the cm handles are saved'
246             objectUnderTest.saveCmHandleBatch([yangModelCmHandle1, yangModelCmHandle2])
247         then: 'CPS Data Service persists both cm handles as a batch'
248             1 * mockCpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
249                     NCMP_DMI_REGISTRY_PARENT, _,null) >> {
250                 args -> {
251                     def jsonData = (args[3] as String)
252                     jsonData.contains('cmhandle1')
253                     jsonData.contains('cmhandle2')
254                 }
255             }
256     }
257
258     def 'Delete list or list elements'() {
259         when: 'the method to delete list or list elements is called'
260             objectUnderTest.deleteListOrListElement('sample xPath')
261         then: 'the data service method to save list elements is called once'
262             1 * mockCpsDataService.deleteListOrListElement(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath',null)
263     }
264
265     def 'Delete schema set with a valid schema set name'() {
266         when: 'the method to delete schema set is called with valid schema set name'
267             objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
268         then: 'the module service to delete schemaSet is invoked once'
269             1 * mockCpsModuleService.deleteSchemaSet(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
270         and: 'the schema set name is validated'
271             1 * mockCpsValidator.validateNameCharacters('validSchemaSetName')
272     }
273
274     def 'Delete multiple schema sets with valid schema set names'() {
275         when: 'the method to delete schema sets is called with valid schema set names'
276             objectUnderTest.deleteSchemaSetsWithCascade(['validSchemaSetName1', 'validSchemaSetName2'])
277         then: 'the module service to delete schema sets is invoked once'
278             1 * mockCpsModuleService.deleteSchemaSetsWithCascade(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['validSchemaSetName1', 'validSchemaSetName2'])
279         and: 'the schema set names are validated'
280             1 * mockCpsValidator.validateNameCharacters(['validSchemaSetName1', 'validSchemaSetName2'])
281     }
282
283     def 'Get data node via xPath'() {
284         when: 'the method to get data nodes is called'
285             objectUnderTest.getDataNode('sample xPath')
286         then: 'the data persistence service method to get data node is invoked once'
287             1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xPath', INCLUDE_ALL_DESCENDANTS)
288     }
289
290     def 'Get cmHandle data node'() {
291         given: 'expected xPath to get cmHandle data node'
292             def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']'
293         when: 'the method to get data nodes is called'
294             objectUnderTest.getCmHandleDataNodeByCmHandleId('sample cmHandleId')
295         then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
296             1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, expectedXPath, INCLUDE_ALL_DESCENDANTS)
297     }
298
299     def 'Get cm handle data node'() {
300         given: 'expected xPath to get cmHandle data node'
301             def expectedXPath = '/dmi-registry/cm-handles[@alternate-id=\'alternate id\']'
302         and: 'query service is invoked with expected xpath'
303             mockCmHandleQueries.queryNcmpRegistryByCpsPath(expectedXPath, OMIT_DESCENDANTS) >> [new DataNode()]
304         expect: 'getting the cm handle data node'
305             assert objectUnderTest.getCmHandleDataNodeByAlternateId('alternate id') == new DataNode()
306     }
307
308     def 'Attempt to get non existing cm handle data node by alternate id'() {
309         given: 'query service is invoked and returns empty collection of data nodes'
310             mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> []
311         when: 'getting the cm handle data node'
312             objectUnderTest.getCmHandleDataNodeByAlternateId('alternate id')
313         then: 'no data found exception thrown'
314             def thrownException = thrown(DataNodeNotFoundException)
315             assert thrownException.getMessage().contains('DataNode not found')
316     }
317
318     def 'Get CM handles that has given module names'() {
319         when: 'the method to get cm handles is called'
320             objectUnderTest.getCmHandleIdsWithGivenModules(['sample-module-name'])
321         then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
322             1 * mockCpsAnchorService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, ['sample-module-name'])
323     }
324
325     def 'Replace list content'() {
326         when: 'replace list content method is called with xpath and data nodes collection'
327             objectUnderTest.replaceListContent('sample xpath', [new DataNode()])
328         then: 'the cps data service method to replace list content is invoked once with same parameters'
329             1 * mockCpsDataService.replaceListContent(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,'sample xpath', [new DataNode()], NO_TIMESTAMP);
330     }
331
332     def 'Delete data node via xPath'() {
333         when: 'Delete data node method is called with xpath as parameter'
334             objectUnderTest.deleteDataNode('sample dataNode xpath')
335         then: 'the cps data service method to delete data node is invoked once with the same xPath'
336             1 * mockCpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, 'sample dataNode xpath', NO_TIMESTAMP);
337     }
338
339     def 'Delete multiple data nodes via xPath'() {
340         when: 'Delete data nodes method is called with multiple xpaths as parameters'
341             objectUnderTest.deleteDataNodes(['xpath1', 'xpath2'])
342         then: 'the cps data service method to delete data nodes is invoked once with the same xPaths'
343             1 * mockCpsDataService.deleteDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, ['xpath1', 'xpath2'], NO_TIMESTAMP);
344     }
345 }