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.databind.ObjectMapper
25 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
26 import org.onap.cps.api.CpsAdminService
27 import org.onap.cps.api.CpsDataService
28 import org.onap.cps.api.CpsModuleService
29 import org.onap.cps.ncmp.api.impl.exception.DmiRequestException
30 import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
31 import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations
32 import org.onap.cps.ncmp.api.impl.operations.YangModelCmHandleRetriever
33 import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse
34 import org.onap.cps.ncmp.api.models.DmiPluginRegistration
35 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
36 import org.onap.cps.spi.exceptions.AlreadyDefinedException
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.CM_HANDLE_ALREADY_EXIST
46 import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_INVALID_ID
47 import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.UNKNOWN_ERROR
48 import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status
49 import static org.onap.cps.spi.CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED
51 class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
54 def ncmpServiceCmHandle = new NcmpServiceCmHandle()
57 def cmHandlesArray = ['cmHandle001']
59 def mockCpsDataService = Mock(CpsDataService)
60 def mockCpsModuleService = Mock(CpsModuleService)
61 def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
62 def mockCpsAdminService = Mock(CpsAdminService)
63 def mockDmiModelOperations = Mock(DmiModelOperations)
64 def mockDmiDataOperations = Mock(DmiDataOperations)
65 def mockNetworkCmProxyDataServicePropertyHandler = Mock(NetworkCmProxyDataServicePropertyHandler)
66 def mockYangModelCmHandleRetriever = Mock(YangModelCmHandleRetriever)
68 def noTimestamp = null
69 def objectUnderTest = getObjectUnderTestWithModelSyncDisabled()
71 def 'DMI Registration: Create, Update & Delete operations are processed in the right order'() {
72 given: 'a registration with operations of all three types'
73 def dmiRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
74 dmiRegistration.setCreatedCmHandles([new NcmpServiceCmHandle(cmHandleID: 'cmhandle-1', publicProperties: ['publicProp1': 'value'], dmiProperties: [:])])
75 dmiRegistration.setUpdatedCmHandles([new NcmpServiceCmHandle(cmHandleID: 'cmhandle-2', publicProperties: ['publicProp1': 'value'], dmiProperties: [:])])
76 dmiRegistration.setRemovedCmHandles(['cmhandle-2'])
77 when: 'registration is processed'
78 objectUnderTest.updateDmiRegistrationAndSyncModule(dmiRegistration)
79 // Spock validated invocation order between multiple then blocks
80 then: 'cm-handles are removed first'
81 1 * objectUnderTest.parseAndRemoveCmHandlesInDmiRegistration(*_)
82 then: 'cm-handles are created'
83 1 * objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(*_)
84 then: 'cm-handles are updated'
85 1 * mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(*_)
88 def 'DMI Registration: Response from all operations types are in response'() {
89 given: 'a registration with operations of all three types'
90 def dmiRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
91 dmiRegistration.setCreatedCmHandles([new NcmpServiceCmHandle(cmHandleID: 'cmhandle-1', publicProperties: ['publicProp1': 'value'], dmiProperties: [:])])
92 dmiRegistration.setUpdatedCmHandles([new NcmpServiceCmHandle(cmHandleID: 'cmhandle-2', publicProperties: ['publicProp1': 'value'], dmiProperties: [:])])
93 dmiRegistration.setRemovedCmHandles(['cmhandle-2'])
94 and: 'update cm-handles can be processed successfully'
95 def updateResponses = [CmHandleRegistrationResponse.createSuccessResponse('cmhandle-2')]
96 mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(*_) >> updateResponses
97 and: 'create cm-handles can be processed successfully'
98 def createdResponses = [CmHandleRegistrationResponse.createSuccessResponse('cmhandle-1')]
99 objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(*_) >> createdResponses
100 and: 'delete cm-handles can be processed successfully'
101 def removeResponses = [CmHandleRegistrationResponse.createSuccessResponse('cmhandle-3')]
102 objectUnderTest.parseAndRemoveCmHandlesInDmiRegistration(*_) >> removeResponses
103 when: 'registration is processed'
104 def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiRegistration)
105 then: 'response has values from all operations'
106 response.getRemovedCmHandles() == removeResponses
107 response.getCreatedCmHandles() == createdResponses
108 response.getUpdatedCmHandles() == updateResponses
113 def 'Create CM-handle Validation: Registration with valid Service names: #scenario'() {
114 given: 'a registration '
115 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: dmiPlugin, dmiModelPlugin: dmiModelPlugin,
116 dmiDataPlugin: dmiDataPlugin)
117 dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
118 when: 'update registration and sync module is called with correct DMI plugin information'
119 objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
120 then: 'create cm handles registration and sync modules is called with the correct plugin information'
121 1 * objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(dmiPluginRegistration)
123 scenario | dmiPlugin | dmiModelPlugin | dmiDataPlugin
124 'combined DMI plugin' | 'service1' | '' | ''
125 'data & model DMI plugins' | '' | 'service1' | 'service2'
126 'data & model using same service' | '' | 'service1' | 'service1'
129 def 'Create CM-handle Validation: Invalid DMI plugin service name with #scenario'() {
130 given: 'a registration '
131 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: dmiPlugin, dmiModelPlugin: dmiModelPlugin,
132 dmiDataPlugin: dmiDataPlugin)
133 dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
134 when: 'registration is called with incorrect DMI plugin information'
135 objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
136 then: 'a DMI Request Exception is thrown with correct message details'
137 def exceptionThrown = thrown(DmiRequestException.class)
138 assert exceptionThrown.getMessage().contains(expectedMessageDetails)
139 and: 'registration is not called'
140 0 * objectUnderTest.parseAndCreateCmHandlesInDmiRegistrationAndSyncModules(dmiPluginRegistration)
142 scenario | dmiPlugin | dmiModelPlugin | dmiDataPlugin || expectedMessageDetails
143 'empty DMI plugins' | '' | '' | '' || 'No DMI plugin service names'
144 'blank DMI plugins' | ' ' | ' ' | ' ' || 'No DMI plugin service names'
145 'null DMI plugins' | null | null | null || 'No DMI plugin service names'
146 'all DMI plugins' | 'service1' | 'service2' | 'service3' || 'Cannot register combined plugin service name and other service names'
147 '(combined)DMI and Data Plugin' | 'service1' | '' | 'service2' || 'Cannot register combined plugin service name and other service names'
148 '(combined)DMI and model Plugin' | 'service1' | 'service2' | '' || 'Cannot register combined plugin service name and other service names'
149 'only model DMI plugin' | '' | 'service1' | '' || 'Cannot register just a Data or Model plugin service name'
150 'only data DMI plugin' | '' | '' | 'service1' || 'Cannot register just a Data or Model plugin service name'
153 def 'Create CM-Handle Successfully: #scenario.'() {
154 given: 'a registration without cm-handle properties'
155 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
156 dmiPluginRegistration.createdCmHandles = [new NcmpServiceCmHandle(cmHandleID: 'cmhandle', dmiProperties: dmiProperties, publicProperties: publicProperties)]
157 when: 'registration is updated'
158 def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
159 then: 'a successful response is received'
160 response.getCreatedCmHandles().size() == 1
161 with(response.getCreatedCmHandles().get(0)) {
162 assert it.status == Status.SUCCESS
163 assert it.cmHandle == 'cmhandle'
165 and: 'save list elements is invoked with the expected parameters'
167 def expectedJsonData = """{"cm-handles":[{"id":"cmhandle","dmi-service-name":"my-server","additional-properties":$expectedDmiProperties,"public-properties":$expectedPublicProperties}]}"""
168 1 * mockCpsDataService.saveListElements('NCMP-Admin', 'ncmp-dmi-registry',
169 '/dmi-registry', expectedJsonData, noTimestamp)
171 then: 'model sync is invoked with expected parameters'
172 1 * objectUnderTest.syncModulesAndCreateAnchor(_) >> { YangModelCmHandle yangModelCmHandle ->
174 assert yangModelCmHandle.id == 'cmhandle'
175 assert yangModelCmHandle.dmiServiceName == 'my-server'
176 assert spiedJsonObjectMapper.asJsonString(yangModelCmHandle.getPublicProperties()) == expectedPublicProperties
177 assert spiedJsonObjectMapper.asJsonString(yangModelCmHandle.getDmiProperties()) == expectedDmiProperties
182 scenario | dmiProperties | publicProperties || expectedDmiProperties | expectedPublicProperties
183 'with dmi & public properties' | ['dmi-key': 'dmi-value'] | ['public-key': 'public-value'] || '[{"name":"dmi-key","value":"dmi-value"}]' | '[{"name":"public-key","value":"public-value"}]'
184 'with only public properties' | [:] | ['public-key': 'public-value'] || '[]' | '[{"name":"public-key","value":"public-value"}]'
185 'with only dmi properties' | ['dmi-key': 'dmi-value'] | [:] || '[{"name":"dmi-key","value":"dmi-value"}]' | '[]'
186 'without dmi & public properties' | [:] | [:] || '[]' | '[]'
190 def 'Create CM-Handle Multiple Requests: All cm-handles creation requests are processed'() {
191 given: 'a registration with three cm-handles to be created'
192 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
193 createdCmHandles: [new NcmpServiceCmHandle(cmHandleID: 'cmhandle1'),
194 new NcmpServiceCmHandle(cmHandleID: 'cmhandle2'),
195 new NcmpServiceCmHandle(cmHandleID: 'cmhandle3')])
196 and: 'cm-handle creation is successful for 1st and 3rd; failed for 2nd'
197 mockCpsDataService.saveListElements(_, _, _, _, _) >> {} >> { throw new RuntimeException("Failed") } >> {}
198 when: 'registration is updated to create cm-handles'
199 def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
200 then: 'a response is received for all cm-handles'
201 response.getCreatedCmHandles().size() == 3
202 and: '1st and 3rd cm-handle are created successfully'
203 with(response.getCreatedCmHandles().get(0)) {
204 assert it.status == Status.SUCCESS
205 assert it.cmHandle == 'cmhandle1'
207 with(response.getCreatedCmHandles().get(2)) {
208 assert it.status == Status.SUCCESS
209 assert it.cmHandle == 'cmhandle3'
211 and: '2nd cm-handle creation fails'
212 with(response.getCreatedCmHandles().get(1)) {
213 assert it.status == Status.FAILURE
214 assert it.registrationError == UNKNOWN_ERROR
215 assert it.errorText == 'Failed'
216 assert it.cmHandle == 'cmhandle2'
220 def 'Create CM-Handle Error Handling: Registration fails: #scenario'() {
221 given: 'a registration without cm-handle properties'
222 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
223 dmiPluginRegistration.createdCmHandles = [new NcmpServiceCmHandle(cmHandleID: cmHandleId)]
224 and: 'cm-handler registration fails: #scenario'
225 mockCpsDataService.saveListElements(_, _, _, _, _) >> { throw exception }
226 when: 'registration is updated'
227 def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
228 then: 'a failure response is received'
229 response.getCreatedCmHandles().size() == 1
230 with(response.getCreatedCmHandles().get(0)) {
231 assert it.status == Status.FAILURE
232 assert it.cmHandle == cmHandleId
233 assert it.registrationError == expectedError
234 assert it.errorText == expectedErrorText
236 and: 'model-sync is not invoked'
237 0 * objectUnderTest.syncModulesAndCreateAnchor(_)
239 scenario | cmHandleId | exception || expectedError | expectedErrorText
240 'cm-handle already exist' | 'cmhandle' | new AlreadyDefinedException('', new RuntimeException()) || CM_HANDLE_ALREADY_EXIST | 'cm-handle already exists'
241 'cm-handle has invalid name' | 'cm handle with space' | new DataValidationException("", "") || CM_HANDLE_INVALID_ID | 'cm-handle has an invalid character(s) in id'
242 'unknown exception while registering cm-handle' | 'cmhandle' | new RuntimeException('Failed') || UNKNOWN_ERROR | 'Failed'
245 def 'Create CM-Handle Error Handling: Model Sync fails'() {
246 given: 'objects under test without disabled model sync'
247 def objectUnderTest = getObjectUnderTest()
248 and: 'a registration without cm-handle properties'
249 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
250 dmiPluginRegistration.createdCmHandles = [new NcmpServiceCmHandle(cmHandleID: 'cmhandle')]
251 and: 'cm-handler models sync fails'
252 objectUnderTest.syncModulesAndCreateAnchor(*_) >> { throw new RuntimeException('Model-Sync failed') }
253 when: 'registration is updated'
254 def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
255 then: 'a failure response is received'
256 response.getCreatedCmHandles().size() == 1
257 with(response.getCreatedCmHandles().get(0)) {
258 assert it.status == Status.FAILURE
259 assert it.cmHandle == 'cmhandle'
260 assert it.registrationError == UNKNOWN_ERROR
261 assert it.errorText == 'Model-Sync failed'
263 and: 'cm-handle is registered'
264 1 * mockCpsDataService.saveListElements(*_)
267 def 'Update CM-Handle: Update Operation Response is added to the response'() {
268 given: 'a registration to update CmHandles'
269 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
270 updatedCmHandles: [{}])
271 and: 'cm-handle updates can be processed successfully'
272 def updateOperationResponse = [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-1'),
273 CmHandleRegistrationResponse.createFailureResponse('cm-handle-2', new Exception("Failed")),
274 CmHandleRegistrationResponse.createFailureResponse('cm-handle-3', CM_HANDLE_DOES_NOT_EXIST),
275 CmHandleRegistrationResponse.createFailureResponse('cm handle 4', CM_HANDLE_INVALID_ID)]
276 mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(_) >> updateOperationResponse
277 when: 'registration is updated'
278 def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
279 then: 'the response contains updateOperationResponse'
280 assert response.getUpdatedCmHandles().size() == 4
281 assert response.getUpdatedCmHandles().containsAll(updateOperationResponse)
284 def 'Remove CmHandle Successfully: #scenario'() {
285 given: 'a registration'
286 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
287 removedCmHandles: ['cmhandle'])
289 mockCpsModuleService.deleteSchemaSet(_, 'cmhandle', CASCADE_DELETE_ALLOWED) >>
290 { if (!schemaSetExist) { throw new SchemaSetNotFoundException("", "") } }
291 when: 'registration is updated to delete cmhandle'
292 def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
293 then: 'delete list or list element is called'
294 1 * mockCpsDataService.deleteListOrListElement(_, _, _, _)
295 and: 'successful response is received'
296 assert response.getRemovedCmHandles().size() == 1
297 with(response.getRemovedCmHandles().get(0)) {
298 assert it.status == Status.SUCCESS
299 assert it.cmHandle == 'cmhandle'
302 scenario | schemaSetExist
303 'schema-set exists and can be deleted successfully' | true
304 'schema-set does not exist' | false
307 def 'Remove CmHandle: All cm-handles delete requests are processed'() {
308 given: 'a registration with three cm-handles to be deleted'
309 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
310 removedCmHandles: ['cmhandle1', 'cmhandle2', 'cmhandle3'])
311 and: 'cm-handle deletion is successful for 1st and 3rd; failed for 2nd'
312 mockCpsDataService.deleteListOrListElement(_, _, _, _) >> {} >> { throw new RuntimeException("Failed") } >> {}
313 when: 'registration is updated to delete cmhandles'
314 def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
315 then: 'a response is received for all cm-handles'
316 response.getRemovedCmHandles().size() == 3
317 and: '1st and 3rd cm-handle deletes successfully'
318 with(response.getRemovedCmHandles().get(0)) {
319 assert it.status == Status.SUCCESS
320 assert it.cmHandle == 'cmhandle1'
322 with(response.getRemovedCmHandles().get(2)) {
323 assert it.status == Status.SUCCESS
324 assert it.cmHandle == 'cmhandle3'
326 and: '2nd cm-handle deletion fails'
327 with(response.getRemovedCmHandles().get(1)) {
328 assert it.status == Status.FAILURE
329 assert it.registrationError == UNKNOWN_ERROR
330 assert it.errorText == 'Failed'
331 assert it.cmHandle == 'cmhandle2'
335 def 'Remove CmHandle Error Handling: Schema Set Deletion failed'() {
336 given: 'a registration'
337 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
338 removedCmHandles: ['cmhandle'])
339 and: 'schema set deletion failed with unknown error'
340 mockCpsModuleService.deleteSchemaSet(_, _, _) >> { throw new RuntimeException('Failed') }
341 when: 'registration is updated to delete cmhandle'
342 def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
343 then: 'no exception is thrown'
345 and: 'cm-handle is not deleted'
346 0 * mockCpsDataService.deleteListOrListElement(_, _, _, _)
347 and: 'a failure response is received'
348 assert response.getRemovedCmHandles().size() == 1
349 with(response.getRemovedCmHandles().get(0)) {
350 assert it.status == Status.FAILURE
351 assert it.cmHandle == 'cmhandle'
352 assert it.errorText == 'Failed'
353 assert it.registrationError == UNKNOWN_ERROR
357 def 'Remove CmHandle Error Handling: #scenario'() {
358 given: 'a registration'
359 def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
360 removedCmHandles: ['cmhandle'])
361 and: 'cm-handle deletion throws exception'
362 mockCpsDataService.deleteListOrListElement(_, _, _, _) >> { throw deleteListElementException }
363 when: 'registration is updated to delete cmhandle'
364 def response = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
365 then: 'no exception is thrown'
367 and: 'a failure response is received'
368 assert response.getRemovedCmHandles().size() == 1
369 with(response.getRemovedCmHandles().get(0)) {
370 assert it.status == Status.FAILURE
371 assert it.cmHandle == 'cmhandle'
372 assert it.registrationError == expectedError
373 assert it.errorText == expectedErrorText
376 scenario | cmHandleId | deleteListElementException || expectedError | expectedErrorText
377 'cm-handle does not exist' | 'cmhandle' | new DataNodeNotFoundException("", "", "") || CM_HANDLE_DOES_NOT_EXIST | 'cm-handle does not exist'
378 'cm-handle has invalid name' | 'cm handle with space' | new DataValidationException("", "") || CM_HANDLE_INVALID_ID | 'cm-handle has an invalid character(s) in id'
379 'an unexpected exception' | 'cmhandle' | new RuntimeException("Failed") || UNKNOWN_ERROR | 'Failed'
382 def getObjectUnderTestWithModelSyncDisabled() {
383 def objectUnderTest = getObjectUnderTest()
384 objectUnderTest.syncModulesAndCreateAnchor(*_) >> null
385 return objectUnderTest
388 def getObjectUnderTest() {
389 return Spy(new NetworkCmProxyDataServiceImpl(mockCpsDataService, spiedJsonObjectMapper, mockDmiDataOperations, mockDmiModelOperations,
390 mockCpsModuleService, mockCpsAdminService, mockNetworkCmProxyDataServicePropertyHandler, mockYangModelCmHandleRetriever))