Merge "Distributed map setup for Module and Data Sync"
[cps.git] / cps-ncmp-service / src / test / groovy / org / onap / cps / ncmp / api / inventory / InventoryPersistenceSpec.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.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
45 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
46
47 class InventoryPersistenceSpec 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 InventoryPersistence(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     @Shared
83     def static sampleDataNodes = [new DataNode()]
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         where: 'the following parameters are used'
102             scenario                    | childDataNodes                                || expectedDmiProperties                               || expectedPublicProperties                              || expectedCompositeState
103             'no properties'             | []                                            || []                                                  || []                                                    || null
104             'DMI and public properties' | childDataNodesForCmHandleWithAllProperties    || [new YangModelCmHandle.Property("name1", "value1")] || [new YangModelCmHandle.Property("name2", "value2")] || null
105             'just DMI properties'       | childDataNodesForCmHandleWithDMIProperties    || [new YangModelCmHandle.Property("name1", "value1")] || []                                                    || null
106             'just public properties'    | childDataNodesForCmHandleWithPublicProperties || []                                                  || [new YangModelCmHandle.Property("name2", "value2")]   || null
107             'with state details'        | childDataNodesForCmHandleWithState            || []                                                  || []                                                    || CmHandleState.ADVISED
108     }
109
110     def "Retrieve CmHandle using datanode with invalid CmHandle id."() {
111         when: 'retrieving the yang modelled cm handle with an invalid id'
112             def result = objectUnderTest.getYangModelCmHandle('cm handle id with spaces')
113         then: 'a data validation exception is thrown'
114             thrown(DataValidationException)
115         and: 'the result is not returned'
116             result == null
117     }
118
119     def "Handling missing service names as null CPS-1043."() {
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: [:])
122             mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
123         when: 'retrieving the yang modelled cm handle'
124             def result = objectUnderTest.getYangModelCmHandle(cmHandleId)
125         then: 'the service names ae returned as null'
126             result.dmiServiceName == null
127             result.dmiDataServiceName == null
128             result.dmiModelServiceName == null
129     }
130
131     def 'Get a Cm Handle Composite State'() {
132         given: 'a valid cm handle id'
133             def cmHandleId = 'Some-Cm-Handle'
134             def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
135         and: 'cps data service returns a valid data node'
136             mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
137                     '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
138         when: 'get cm handle state is invoked'
139             def result = objectUnderTest.getCmHandleState(cmHandleId)
140         then: 'result has returned the correct cm handle state'
141             result.cmHandleState == CmHandleState.ADVISED
142     }
143
144     def 'Update Cm Handle with #scenario State'() {
145         given: 'a cm handle and a composite state'
146             def cmHandleId = 'Some-Cm-Handle'
147             def compositeState = new CompositeState(cmHandleState: cmHandleState, lastUpdateTime: formattedDateAndTime)
148         when: 'update cm handle state is invoked with the #scenario state'
149             objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
150         then: 'update node leaves is invoked with the correct params'
151             1 * mockCpsDataService.replaceNodeTree('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
152         where: 'the following states are used'
153             scenario    | cmHandleState          || expectedJsonData
154             'READY'     | CmHandleState.READY    || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
155             'LOCKED'    | CmHandleState.LOCKED   || '{"state":{"cm-handle-state":"LOCKED","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
156             'DELETING'  | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
157     }
158
159     def 'Get Cm Handles By State'() {
160         given: 'a cm handle state to query'
161             def cmHandleState = CmHandleState.ADVISED
162         and: 'cps data service returns a list of data nodes'
163             mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
164                 '//state[@cm-handle-state="ADVISED"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
165         when: 'get cm handles by state is invoked'
166             def result = objectUnderTest.getCmHandlesByState(cmHandleState)
167         then: 'the returned result is a list of data nodes returned by cps data service'
168             assert result == sampleDataNodes
169     }
170
171     def 'Get Cm Handles By State and Cm-Handle Id'() {
172         given: 'a cm handle state to query'
173             def cmHandleState = CmHandleState.READY
174         and: 'cps data service returns a list of data nodes'
175             mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
176                 '//cm-handles[@id=\'some-cm-handle\']/state[@cm-handle-state="'+ 'READY'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
177         when: 'get cm handles by state and id is invoked'
178             def result = objectUnderTest.getCmHandlesByIdAndState(cmHandleId, cmHandleState)
179         then: 'the returned result is a list of data nodes returned by cps data service'
180             assert result == sampleDataNodes
181     }
182
183     def 'Get Cm Handles By Operational Sync State : UNSYNCHRONIZED'() {
184         given: 'a cm handle state to query'
185             def cmHandleState = CmHandleState.READY
186         and: 'cps data service returns a list of data nodes'
187             mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
188                 '//state/datastores/operational[@sync-state="'+'UNSYNCHRONIZED'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
189         when: 'get cm handles by operational sync state as UNSYNCHRONIZED is invoked'
190             def result = objectUnderTest.getCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED)
191         then: 'the returned result is a list of data nodes returned by cps data service'
192             assert result == sampleDataNodes
193     }
194
195     def 'Retrieve cm handle by cps path '() {
196         given: 'a cm handle state to query based on the cps path'
197             def cmHandleDataNode = new DataNode(xpath: 'xpath', leaves: ['cm-handle-state': 'LOCKED'])
198             def cpsPath = '//cps-path'
199         and: 'cps data service returns a valid data node'
200             mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
201                     cpsPath, INCLUDE_ALL_DESCENDANTS)
202                     >> Arrays.asList(cmHandleDataNode)
203         when: 'get cm handles by cps path is invoked'
204             def result = objectUnderTest.getCmHandleDataNodesByCpsPath(cpsPath, INCLUDE_ALL_DESCENDANTS)
205         then: 'the returned result is a list of data nodes returned by cps data service'
206             assert result.contains(cmHandleDataNode)
207     }
208
209     def 'Get module definitions'() {
210         given: 'cps module service returns a collection of module definitions'
211             def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
212             mockCpsModuleService.getModuleDefinitionsByAnchorName('NFP-Operational','some-cmHandle-Id') >> moduleDefinitions
213         when: 'get module definitions by cmHandle is invoked'
214             def result = objectUnderTest.getModuleDefinitionsByCmHandleId('some-cmHandle-Id')
215         then: 'the returned result are the same module definitions as returned from the module service'
216             assert result == moduleDefinitions
217     }
218
219     def 'Get module references'() {
220         given: 'cps module service returns a collection of module references'
221             def moduleReferences = [new ModuleReference('moduleName','revision','namespace')]
222             mockCpsModuleService.getYangResourcesModuleReferences('NFP-Operational','some-cmHandle-Id') >> moduleReferences
223         when: 'get yang resources module references by cmHandle is invoked'
224             def result = objectUnderTest.getYangResourcesModuleReferences('some-cmHandle-Id')
225         then: 'the returned result is a collection of module definitions'
226             assert result == moduleReferences
227     }
228
229     def 'Save list elements'() {
230         when: 'the method to save list elements is called'
231             objectUnderTest.saveListElements('sample Json data')
232         then: 'the data service method to save list elements is called once'
233             1 * mockCpsDataService.saveListElements('NCMP-Admin','ncmp-dmi-registry','/dmi-registry','sample Json data',null)
234     }
235
236     def 'Delete list or list elements'() {
237         when: 'the method to delete list or list elements is called'
238             objectUnderTest.deleteListOrListElement('sample xPath')
239         then: 'the data service method to save list elements is called once'
240             1 * mockCpsDataService.deleteListOrListElement('NCMP-Admin','ncmp-dmi-registry','sample xPath',null)
241     }
242
243     def 'Delete schema set with a valid schema set name'() {
244         when: 'the method to delete schema set is called with valid schema set name'
245             objectUnderTest.deleteSchemaSetWithCascade('validSchemaSetName')
246         then: 'the module service to delete schemaSet is invoked once'
247             1 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'validSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
248     }
249
250     def 'Delete schema set with an invalid schema set name'() {
251         when: 'the method to delete schema set is called with an invalid schema set name'
252             objectUnderTest.deleteSchemaSetWithCascade('invalid SchemaSet name')
253         then: 'a data validation exception is thrown'
254             thrown(DataValidationException)
255         and: 'the module service to delete schemaSet is not called'
256             0 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'sampleSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
257     }
258
259     def 'Query data nodes via cpsPath'() {
260         when: 'the method to query data nodes is called'
261             objectUnderTest.queryDataNodes('sample cpsPath')
262         then: 'the data persistence service method to query data nodes is invoked once'
263             1 * mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin','ncmp-dmi-registry','sample cpsPath', INCLUDE_ALL_DESCENDANTS)
264     }
265
266     def 'Get data node via xPath'() {
267         when: 'the method to get data nodes is called'
268             objectUnderTest.getDataNode('sample xPath')
269         then: 'the data persistence service method to get data node is invoked once'
270             1 * mockCpsDataPersistenceService.getDataNode('NCMP-Admin','ncmp-dmi-registry','sample xPath', INCLUDE_ALL_DESCENDANTS)
271     }
272
273     def 'Query anchors'() {
274         when: 'the method to query anchors is called'
275             objectUnderTest.queryAnchors(['sample-module-name'])
276         then: 'the admin persistence service method to query anchors is invoked once with the same parameter'
277             1 * mockCpsAdminPersistenceService.queryAnchors('NFP-Operational',['sample-module-name'])
278     }
279
280     def 'Get anchors'() {
281         when: 'the method to get anchors with no parameters is called'
282           objectUnderTest.getAnchors()
283         then: 'the admin persistence service method to query anchors is invoked once with a specific dataspace name'
284             1 * mockCpsAdminPersistenceService.getAnchors('NFP-Operational')
285     }
286 }