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
10 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 * SPDX-License-Identifier: Apache-2.0
19 * ============LICENSE_END=========================================================
22 package org.onap.cps.ncmp.api.impl
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
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
48 class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
51 def ncmpServiceCmHandle = new NcmpServiceCmHandle()
54 def cmHandlesArray = ['cmHandle001']
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)
65 def noTimestamp = null
66 def objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
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(*_)
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"}]' +
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)
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
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)
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)
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)
163 def 'Remove CmHandle Successfully: #scenario'() {
164 given: 'a registration'
165 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
166 removedCmHandles: ['cmhandle'])
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'
181 scenario | schemaSetExist
182 'schema-set exists and can be deleted successfully' | true
183 'schema-set does not exist' | false
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
200 with(response.getRemovedCmHandles().get(2)) {
201 assert it.status == CmHandleRegistrationResponse.Status.SUCCESS
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'
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'
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
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'
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
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'
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)
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'
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)
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'
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