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