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