Registration Response for Update and Delete cmhandles operations
[cps.git] / cps-ncmp-service / src / test / groovy / org / onap / cps / ncmp / api / impl / NetworkCmProxyDataServiceImplRegistrationSpec.groovy
1 /*
2  * ============LICENSE_START=======================================================
3  *  Copyright (C) 2021-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.impl
23
24 import com.fasterxml.jackson.core.JsonProcessingException
25 import com.fasterxml.jackson.databind.ObjectMapper
26 import java.util.function.Predicate
27 import org.onap.cps.api.CpsAdminService
28 import org.onap.cps.api.CpsDataService
29 import org.onap.cps.api.CpsModuleService
30 import org.onap.cps.ncmp.api.impl.exception.DmiRequestException
31 import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
32 import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations
33 import org.onap.cps.ncmp.api.impl.operations.YangModelCmHandleRetriever
34 import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse
35 import org.onap.cps.ncmp.api.models.DmiPluginRegistration
36 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
37 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
38 import org.onap.cps.spi.exceptions.DataValidationException
39 import org.onap.cps.spi.exceptions.SchemaSetNotFoundException
40 import org.onap.cps.utils.JsonObjectMapper
41 import spock.lang.Shared
42 import spock.lang.Specification
43
44 import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_DOES_NOT_EXIST
45 import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.UNKNOWN_ERROR
46 import static org.onap.cps.spi.CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED
47
48 class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
49
50     @Shared
51     def ncmpServiceCmHandle = new NcmpServiceCmHandle()
52
53     @Shared
54     def cmHandlesArray = ['cmHandle001']
55
56     def mockCpsDataService = Mock(CpsDataService)
57     def mockCpsModuleService = Mock(CpsModuleService)
58     def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
59     def mockCpsAdminService = Mock(CpsAdminService)
60     def mockDmiModelOperations = Mock(DmiModelOperations)
61     def mockDmiDataOperations = Mock(DmiDataOperations)
62     def mockNetworkCmProxyDataServicePropertyHandler = Mock(NetworkCmProxyDataServicePropertyHandler)
63     def mockYangModelCmHandleRetriever = Mock(YangModelCmHandleRetriever)
64
65     def noTimestamp = null
66     def objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
67
68     def 'DMI Registration: Create, Update & Delete operations are processed in the right order'() {
69         given: 'a registration with operations of all three types'
70             def dmiRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
71             dmiRegistration.setCreatedCmHandles([new NcmpServiceCmHandle(cmHandleID: 'cmhandle-1', publicProperties: ['publicProp1': 'value'], dmiProperties: [:])])
72             dmiRegistration.setUpdatedCmHandles([new NcmpServiceCmHandle(cmHandleID: 'cmhandle-2', publicProperties: ['publicProp1': 'value'], dmiProperties: [:])])
73             dmiRegistration.setRemovedCmHandles(['cmhandle-2'])
74         when: 'registration is processed'
75             objectUnderTest.updateDmiRegistrationAndSyncModule(dmiRegistration)
76             // Spock validated invocation order between multiple then blocks
77         then: 'cm-handles are removed first'
78             1 * mockCpsDataService.deleteListOrListElement(*_)
79         then: 'cm-handles are created'
80             1 * mockCpsDataService.saveListElements(*_)
81         then: 'cm-handles are updated'
82             1 * mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(*_)
83     }
84
85     def 'Register or re-register a DMI Plugin for the given cm-handle(s) with #scenario process.'() {
86         given: 'a registration'
87             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
88             ncmpServiceCmHandle.cmHandleID = '123'
89             ncmpServiceCmHandle.dmiProperties = [dmiProp1: 'dmiValue1', dmiProp2: 'dmiValue2']
90             ncmpServiceCmHandle.publicProperties = [publicProp1: 'publicValue1', publicProp2: 'publicValue2']
91             dmiPluginRegistration.createdCmHandles = createdCmHandles
92             dmiPluginRegistration.updatedCmHandles = updatedCmHandles
93             dmiPluginRegistration.removedCmHandles = removedCmHandles
94             def expectedJsonData = '{"cm-handles":[{"id":"123","dmi-service-name":"my-server","dmi-data-service-name":null,"dmi-model-service-name":null,' +
95                 '"additional-properties":[{"name":"dmiProp1","value":"dmiValue1"},{"name":"dmiProp2","value":"dmiValue2"}],' +
96                 '"public-properties":[{"name":"publicProp1","value":"publicValue1"},{"name":"publicProp2","value":"publicValue2"}]' +
97                 '}]}'
98         when: 'registration is updated and modules are synced'
99             objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
100         then: 'save list elements is invoked with the expected parameters'
101             expectedCallsToSaveNode * mockCpsDataService.saveListElements('NCMP-Admin', 'ncmp-dmi-registry',
102                 '/dmi-registry', expectedJsonData, noTimestamp)
103         and: 'update data node leaves is called with correct parameters'
104             expectedCallsToUpdateCmHandleProperty * mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(updatedCmHandles)
105         and: 'delete schema set is invoked with the correct parameters'
106             expectedCallsToDeleteSchemaSetAndListElement * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'cmHandle001', CASCADE_DELETE_ALLOWED)
107         and: 'delete list or list element is invoked with the correct parameters'
108             expectedCallsToDeleteSchemaSetAndListElement * mockCpsDataService.deleteListOrListElement('NCMP-Admin',
109                 'ncmp-dmi-registry', "/dmi-registry/cm-handles[@id='cmHandle001']", noTimestamp)
110         where:
111             scenario                    | createdCmHandles      | updatedCmHandles      | removedCmHandles || expectedCallsToSaveNode | expectedCallsToDeleteSchemaSetAndListElement | expectedCallsToUpdateCmHandleProperty
112             'create'                    | [ncmpServiceCmHandle] | []                    | []               || 1                       | 0                                            | 0
113             'update'                    | []                    | [ncmpServiceCmHandle] | []               || 0                       | 0                                            | 1
114             'delete'                    | []                    | []                    | cmHandlesArray   || 0                       | 1                                            | 0
115             'create, update and delete' | [ncmpServiceCmHandle] | [ncmpServiceCmHandle] | cmHandlesArray   || 1                       | 1                                            | 1
116             'no valid data'             | []                    | []                    | []               || 0                       | 0                                            | 0
117     }
118
119     def 'Create CM-Handle: Register a DMI Plugin for the given cm-handle(s) without DMI properties.'() {
120         given: 'a registration without cm-handle properties'
121             NetworkCmProxyDataServiceImpl objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
122             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
123             ncmpServiceCmHandle.cmHandleID = '123'
124             ncmpServiceCmHandle.dmiProperties = Collections.emptyMap()
125             ncmpServiceCmHandle.publicProperties = Collections.emptyMap()
126             dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
127             def expectedJsonData = '{"cm-handles":[{"id":"123","dmi-service-name":"my-server","dmi-data-service-name":null,"dmi-model-service-name":null,"additional-properties":[],"public-properties":[]}]}'
128         when: 'registration is updated'
129             objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
130         then: 'save list elements is invoked with the expected parameters'
131             1 * mockCpsDataService.saveListElements('NCMP-Admin', 'ncmp-dmi-registry',
132                 '/dmi-registry', expectedJsonData, noTimestamp)
133     }
134
135     def 'Create CM-Handle: Register a DMI Plugin for a given cm-handle(s) with JSON processing errors during process.'() {
136         given: 'a registration without cm-handle properties '
137             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'some-plugin')
138             dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
139         and: 'an json processing exception occurs'
140             spiedJsonObjectMapper.asJsonString(_) >> { throw (new JsonProcessingException('')) }
141         when: 'registration is updated and modules are synced'
142             objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
143         then: 'a data validation exception is thrown'
144             thrown(DataValidationException)
145     }
146
147     def 'Update CM-Handle: Update Operation Response is added to the response'() {
148         given: 'a registration to update CmHandles'
149             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
150                 updatedCmHandles: [{}])
151         and: 'cm-handle updates can be processed successfully'
152             def updateOperationResponse = [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-1'),
153                                            CmHandleRegistrationResponse.createFailureResponse('cm-handle-2', new Exception("Failed")),
154                                            CmHandleRegistrationResponse.createFailureResponse('cm-handle-3', CM_HANDLE_DOES_NOT_EXIST)]
155             mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(_) >> updateOperationResponse
156         when: 'registration is updated'
157             def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
158         then: 'the response contains updateOperationResponse'
159             assert response.getUpdatedCmHandles().size() == 3
160             assert response.getUpdatedCmHandles().containsAll(updateOperationResponse)
161     }
162
163     def 'Remove CmHandle Successfully: #scenario'() {
164         given: 'a registration'
165             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
166                 removedCmHandles: ['cmhandle'])
167         and: '#scenario'
168             mockCpsModuleService.deleteSchemaSet(_, 'cmhandle', CASCADE_DELETE_ALLOWED) >>
169                 { if (!schemaSetExist) { throw new SchemaSetNotFoundException("", "") } }
170         when: 'registration is updated to delete cmhandle'
171             def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
172         then: 'delete list or list element is called'
173             1 * mockCpsDataService.deleteListOrListElement(_, _, _, _)
174         and: 'successful response is received'
175             assert response.getRemovedCmHandles().size() == 1
176             with(response.getRemovedCmHandles().get(0)) {
177                 assert it.status == CmHandleRegistrationResponse.Status.SUCCESS
178                 assert it.cmHandle == 'cmhandle'
179             }
180         where:
181             scenario                                            | schemaSetExist
182             'schema-set exists and can be deleted successfully' | true
183             'schema-set does not exist'                         | false
184     }
185
186     def 'Remove CmHandle: All cm-handles delete requests are processed'() {
187         given: 'a registration with three cm-handles to be deleted'
188             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
189                 removedCmHandles: ['cmhandle1', 'cmhandle2', 'cmhandle3'])
190         and: 'cm-handle deletion is successful for 1st and 3rd; failed for 2nd'
191             mockCpsDataService.deleteListOrListElement(_, _, _, _) >> {} >> { throw new RuntimeException("Failed") } >> {}
192         when: 'registration is updated to delete cmhandles'
193             def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
194         then: 'a response is received for all cm-handles'
195             response.getRemovedCmHandles().size() == 3
196         and: '1st and 3rd cm-handle deletes successfully'
197             with(response.getRemovedCmHandles().get(0)) {
198                 assert it.status == CmHandleRegistrationResponse.Status.SUCCESS
199             }
200             with(response.getRemovedCmHandles().get(2)) {
201                 assert it.status == CmHandleRegistrationResponse.Status.SUCCESS
202             }
203         and: '2nd cmhandle deletion fails'
204             with(response.getRemovedCmHandles().get(1)) {
205                 assert it.status == CmHandleRegistrationResponse.Status.FAILURE
206                 assert it.registrationError == UNKNOWN_ERROR
207                 assert it.errorText == 'Failed'
208             }
209     }
210
211     def 'Remove CmHandle Error Handling: Schema Set Deletion failed'() {
212         given: 'a registration'
213             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
214                 removedCmHandles: ['cmhandle'])
215         and: 'schema set deletion failed with unknown error'
216             mockCpsModuleService.deleteSchemaSet(_, _, _) >> { throw new RuntimeException('Failed') }
217         when: 'registration is updated to delete cmhandle'
218             def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
219         then: 'no exception is thrown'
220             noExceptionThrown()
221         and: 'cm-handle is not deleted'
222             0 * mockCpsDataService.deleteListOrListElement(_, _, _, _)
223         and: 'a failure response is received'
224             assert response.getRemovedCmHandles().size() == 1
225             with(response.getRemovedCmHandles().get(0)) {
226                 assert it.status == CmHandleRegistrationResponse.Status.FAILURE
227                 assert it.cmHandle == 'cmhandle'
228                 assert it.errorText == 'Failed'
229                 assert it.registrationError == UNKNOWN_ERROR
230             }
231     }
232
233     def 'Remove CmHandle Error Handling: #scenario'() {
234         given: 'a registration'
235             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
236                 removedCmHandles: ['cmhandle'])
237         and: 'cm-handle deletion throws exception'
238             mockCpsDataService.deleteListOrListElement(_, _, _, _) >> { throw deleteListElementException }
239         when: 'registration is updated to delete cmhandle'
240             def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
241         then: 'no exception is thrown'
242             noExceptionThrown()
243         and: 'a failure response is received'
244             assert response.getRemovedCmHandles().size() == 1
245             with(response.getRemovedCmHandles().get(0)) {
246                 assert it.status == CmHandleRegistrationResponse.Status.FAILURE
247                 assert it.cmHandle == 'cmhandle'
248                 assert it.registrationError == expectedError
249                 assert it.errorText == expectedErrorText
250             }
251         where:
252             scenario                   | deleteListElementException                | expectedError            | expectedErrorText
253             'cm-handle does not exist' | new DataNodeNotFoundException("", "", "") | CM_HANDLE_DOES_NOT_EXIST | 'cm-handle does not exist'
254             'an unexpected exception'  | new RuntimeException("Failed")            | UNKNOWN_ERROR            | 'Failed'
255     }
256
257     def 'Create CM-handle Validation: Registration with valid Service names: #scenario'() {
258         given: 'a registration '
259             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: dmiPlugin, dmiModelPlugin: dmiModelPlugin,
260                 dmiDataPlugin: dmiDataPlugin)
261             dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
262         when: 'update registration and sync module is called with correct DMI plugin information'
263             objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
264         then: 'create cm handles registration and sync modules is called with the correct plugin information'
265             1 * objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(dmiPluginRegistration)
266         where:
267             scenario                          | dmiPlugin  | dmiModelPlugin | dmiDataPlugin
268             'combined DMI plugin'             | 'service1' | ''             | ''
269             'data & model DMI plugins'        | ''         | 'service1'     | 'service2'
270             'data & model using same service' | ''         | 'service1'     | 'service1'
271     }
272
273     def 'Create CM-handle Error Handling: Invalid DMI plugin service name with #scenario'() {
274         given: 'a registration '
275             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: dmiPlugin, dmiModelPlugin: dmiModelPlugin,
276                 dmiDataPlugin: dmiDataPlugin)
277             dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
278         when: 'registration is called with incorrect DMI plugin information'
279             objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
280         then: 'a DMI Request Exception is thrown with correct message details'
281             def exceptionThrown = thrown(DmiRequestException.class)
282             assert exceptionThrown.getMessage().contains(expectedMessageDetails)
283         and: 'registration is not called'
284             0 * objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(dmiPluginRegistration)
285         where:
286             scenario                         | dmiPlugin  | dmiModelPlugin | dmiDataPlugin || expectedMessageDetails
287             'empty DMI plugins'              | ''         | ''             | ''            || 'No DMI plugin service names'
288             'blank DMI plugins'              | ' '        | ' '            | ' '           || 'No DMI plugin service names'
289             'null DMI plugins'               | null       | null           | null          || 'No DMI plugin service names'
290             'all DMI plugins'                | 'service1' | 'service2'     | 'service3'    || 'Cannot register combined plugin service name and other service names'
291             '(combined)DMI and Data Plugin'  | 'service1' | ''             | 'service2'    || 'Cannot register combined plugin service name and other service names'
292             '(combined)DMI and model Plugin' | 'service1' | 'service2'     | ''            || 'Cannot register combined plugin service name and other service names'
293             'only model DMI plugin'          | ''         | 'service1'     | ''            || 'Cannot register just a Data or Model plugin service name'
294             'only data DMI plugin'           | ''         | ''             | 'service1'    || 'Cannot register just a Data or Model plugin service name'
295     }
296
297     def getObjectUnderTestWithModelSyncDisabled() {
298         def objectUnderTest = Spy(new NetworkCmProxyDataServiceImpl(mockCpsDataService, spiedJsonObjectMapper, mockDmiDataOperations, mockDmiModelOperations,
299             mockCpsModuleService, mockCpsAdminService, mockNetworkCmProxyDataServicePropertyHandler, mockYangModelCmHandleRetriever))
300         objectUnderTest.syncModulesAndCreateAnchor(*_) >> null
301         return objectUnderTest
302     }
303 }