f99fe2d6506e3a83c4b2335a989b7877b19a9bf7
[cps.git] /
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved.
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.impl.inventory
23
24 import com.hazelcast.map.IMap
25 import org.onap.cps.api.CpsDataService
26 import org.onap.cps.api.exceptions.AlreadyDefinedException
27 import org.onap.cps.api.exceptions.CpsException
28 import org.onap.cps.api.exceptions.DataNodeNotFoundException
29 import org.onap.cps.api.exceptions.DataValidationException
30 import org.onap.cps.ncmp.api.exceptions.DmiRequestException
31 import org.onap.cps.ncmp.api.inventory.DataStoreSyncState
32 import org.onap.cps.ncmp.api.inventory.models.CmHandleRegistrationResponse
33 import org.onap.cps.ncmp.api.inventory.models.CompositeState
34 import org.onap.cps.ncmp.api.inventory.models.DmiPluginRegistration
35 import org.onap.cps.ncmp.api.inventory.models.NcmpServiceCmHandle
36 import org.onap.cps.ncmp.api.inventory.models.TrustLevel
37 import org.onap.cps.ncmp.api.inventory.models.UpgradedCmHandles
38 import org.onap.cps.ncmp.api.inventory.models.CmHandleState
39 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
40 import org.onap.cps.ncmp.impl.inventory.sync.lcm.LcmEventsCmHandleStateHandler
41 import org.onap.cps.ncmp.impl.inventory.trustlevel.TrustLevelManager
42 import spock.lang.Specification
43
44 import static org.onap.cps.ncmp.api.NcmpResponseStatus.CM_HANDLES_NOT_FOUND
45 import static org.onap.cps.ncmp.api.NcmpResponseStatus.CM_HANDLE_ALREADY_EXIST
46 import static org.onap.cps.ncmp.api.NcmpResponseStatus.CM_HANDLE_INVALID_ID
47 import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNKNOWN_ERROR
48 import static org.onap.cps.ncmp.api.inventory.models.CmHandleRegistrationResponse.Status
49 import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
50
51 class CmHandleRegistrationServiceSpec extends Specification {
52
53     def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'some-cm-handle-id')
54     def mockNetworkCmProxyDataServicePropertyHandler = Mock(CmHandleRegistrationServicePropertyHandler)
55     def mockInventoryPersistence = Mock(InventoryPersistence)
56     def mockCmHandleQueries = Mock(CmHandleQueryService)
57     def mockLcmEventsCmHandleStateHandler = Mock(LcmEventsCmHandleStateHandler)
58     def mockCpsDataService = Mock(CpsDataService)
59     def mockModuleSyncStartedOnCmHandles = Mock(IMap<String, Object>)
60     def mockTrustLevelManager = Mock(TrustLevelManager)
61     def mockAlternateIdChecker = Mock(AlternateIdChecker)
62     def mockCmHandleIdPerAlternateId = Mock(IMap)
63
64     def objectUnderTest = Spy(new CmHandleRegistrationService(
65         mockNetworkCmProxyDataServicePropertyHandler, mockInventoryPersistence, mockCpsDataService, mockLcmEventsCmHandleStateHandler,
66         mockModuleSyncStartedOnCmHandles, mockTrustLevelManager, mockAlternateIdChecker, mockCmHandleIdPerAlternateId))
67
68     def setup() {
69         // always accept all cm handles
70         mockAlternateIdChecker.getIdsOfCmHandlesWithRejectedAlternateId(*_) >> []
71
72         // always can find all cm handles in DB
73         mockInventoryPersistence.getYangModelCmHandles(_) >> { args -> args[0].collect { new YangModelCmHandle(id:it) } }
74     }
75
76     def 'DMI Registration: Create, Update, Delete & Upgrade operations are processed in the right order'() {
77         given: 'a registration with operations of all types'
78             def dmiRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
79             dmiRegistration.setCreatedCmHandles([new NcmpServiceCmHandle(cmHandleId: 'cmhandle-1', publicProperties: ['publicProp1': 'value'], dmiProperties: [:])])
80             dmiRegistration.setUpdatedCmHandles([new NcmpServiceCmHandle(cmHandleId: 'cmhandle-2', publicProperties: ['publicProp1': 'value'], dmiProperties: [:])])
81             dmiRegistration.setRemovedCmHandles(['cmhandle-3'])
82             dmiRegistration.setUpgradedCmHandles(new UpgradedCmHandles(cmHandles: ['cmhandle-4', 'cmhandle-5'], moduleSetTag: moduleSetTagForUpgrade))
83         and: 'cm handles 2,3 and 4 already exist in the inventory'
84             mockInventoryPersistence.getYangModelCmHandles(['cmhandle-2']) >> [new YangModelCmHandle()]
85             mockInventoryPersistence.getYangModelCmHandles(['cmhandle-3']) >> [new YangModelCmHandle()]
86             mockInventoryPersistence.getYangModelCmHandle('cmhandle-4') >> new YangModelCmHandle(id: 'cmhandle-4', moduleSetTag: '', compositeState: new CompositeState(cmHandleState: CmHandleState.READY))
87         and: 'cm handle 5 also exist but already has the new module set tag (upgrade to)'
88             mockInventoryPersistence.getYangModelCmHandle('cmhandle-5') >> new YangModelCmHandle(id: 'cmhandle-5', moduleSetTag: moduleSetTagForUpgrade , compositeState: new CompositeState(cmHandleState: CmHandleState.READY))
89         and: 'all cm handles are in READY state'
90             mockCmHandleQueries.cmHandleHasState(_, CmHandleState.READY) >> true
91         and: 'cm handle to be removed is in progress map'
92             mockModuleSyncStartedOnCmHandles.containsKey('cmhandle-3') >> true
93         when: 'registration is processed'
94             def result = objectUnderTest.updateDmiRegistration(dmiRegistration)
95         then: 'cm-handles are removed first'
96             1 * objectUnderTest.processRemovedCmHandles(*_)
97         and: 'de-registered cm handle entry is removed from in progress map'
98             1 * mockModuleSyncStartedOnCmHandles.removeAsync('cmhandle-3')
99         then: 'updated cm handles are processed by the property handler service'
100             1 * objectUnderTest.processUpdatedCmHandles(*_)
101             1 * mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(*_) >> [CmHandleRegistrationResponse.createSuccessResponse('cmhandle-2')]
102         then: 'cm-handles are upgraded'
103             1 * objectUnderTest.processUpgradedCmHandles(*_)
104         and: 'result contains the correct cm handles for each operation'
105             assert result.createdCmHandles.cmHandle == ['cmhandle-1']
106             assert result.updatedCmHandles.cmHandle == ['cmhandle-2']
107             assert result.removedCmHandles.cmHandle == ['cmhandle-3']
108             assert result.upgradedCmHandles.cmHandle as Set == ['cmhandle-4', 'cmhandle-5'] as Set
109         where: 'upgrade with and without module set tag'
110             moduleSetTagForUpgrade << ['some tag', '']
111     }
112
113     def 'DMI Registration upgrade operation with upgrade node state #scenario'() {
114         given: 'a registration with upgrade operation'
115             def dmiRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
116             dmiRegistration.setUpgradedCmHandles(new UpgradedCmHandles(cmHandles: ['cmhandle-3'], moduleSetTag: 'some-module-set-tag'))
117         and: 'cm handle has the state #cmHandleState'
118             mockInventoryPersistence.getYangModelCmHandle('cmhandle-3') >> new YangModelCmHandle(id: 'cmhandle-3', moduleSetTag: '', compositeState: new CompositeState(cmHandleState: cmHandleState))
119         when: 'registration is processed'
120             def result = objectUnderTest.updateDmiRegistration(dmiRegistration)
121         then: 'upgrade operation contains expected error code'
122             assert result.upgradedCmHandles[0].status == expectedResponseStatus
123         where: 'the following parameters are used'
124             scenario    | cmHandleState        || expectedResponseStatus
125             'READY'     | CmHandleState.READY  || Status.SUCCESS
126             'Not READY' | CmHandleState.LOCKED || Status.FAILURE
127     }
128
129     def 'DMI Registration upgrade with exception #scenario'() {
130         given: 'a registration with upgrade operation'
131             def dmiRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
132             dmiRegistration.setUpgradedCmHandles(new UpgradedCmHandles(cmHandles: ['cmhandle-3'], moduleSetTag: 'some-module-set-tag'))
133         and: 'exception while checking cm handle state'
134             mockInventoryPersistence.getYangModelCmHandle('cmhandle-3') >> { throw exception }
135         when: 'registration is processed'
136             def result = objectUnderTest.updateDmiRegistration(dmiRegistration)
137         then: 'upgrade operation contains expected error code'
138             assert result.upgradedCmHandles.ncmpResponseStatus.code[0] == expectedErrorCode
139         where: 'the following parameters are used'
140             scenario               | exception                                                                || expectedErrorCode
141             'data node not found'  | new DataNodeNotFoundException('some-dataspace-name', 'some-anchor-name') || '100'
142             'cm handle is invalid' | new DataValidationException('some error message', 'some error details')  || '110'
143     }
144
145     def 'DMI Registration upgrade with exception while updating CM-handle state'() {
146         given: 'a registration with upgrade operation'
147             def dmiRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
148             dmiRegistration.setUpgradedCmHandles(new UpgradedCmHandles(cmHandles: ['cmhandle-3'], moduleSetTag: 'some-module-set-tag'))
149         and: 'cm handle has the state READY'
150             mockInventoryPersistence.getYangModelCmHandle('cmhandle-3') >> new YangModelCmHandle(id: 'cmhandle-3', moduleSetTag: '', compositeState: new CompositeState(cmHandleState: CmHandleState.READY))
151         and: 'exception will occur while updating cm handle state to LOCKED for upgrade'
152             mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(_) >> { throw new RuntimeException() }
153         when: 'registration is processed'
154             def result = objectUnderTest.updateDmiRegistration(dmiRegistration)
155         then: 'upgrade operation contains expected error code'
156             assert result.upgradedCmHandles[0].status == Status.FAILURE
157             assert result.upgradedCmHandles[0].ncmpResponseStatus == UNKNOWN_ERROR
158     }
159
160     def 'Create CM-handle Validation: Registration with valid Service names: #scenario'() {
161         given: 'a registration '
162             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: dmiPlugin, dmiModelPlugin: dmiModelPlugin,
163                 dmiDataPlugin: dmiDataPlugin)
164             dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
165         when: 'update registration and sync module is called with correct DMI plugin information'
166             objectUnderTest.updateDmiRegistration(dmiPluginRegistration)
167         then: 'create cm handles registration and sync modules is called with the correct plugin information'
168             1 * objectUnderTest.processCreatedCmHandles(dmiPluginRegistration, _)
169         where:
170             scenario                          | dmiPlugin  | dmiModelPlugin | dmiDataPlugin || expectedDmiPluginRegisteredName
171             'combined DMI plugin'             | 'service1' | ''             | ''            || 'service1'
172             'data & model DMI plugins'        | ''         | 'service1'     | 'service2'    || 'service2'
173             'data & model using same service' | ''         | 'service1'     | 'service1'    || 'service1'
174     }
175
176     def 'Create CM-handle Validation: Invalid DMI plugin service name with #scenario'() {
177         given: 'a registration '
178             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: dmiPlugin, dmiModelPlugin: dmiModelPlugin,
179                 dmiDataPlugin: dmiDataPlugin)
180             dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
181         when: 'registration is called with incorrect DMI plugin information'
182             objectUnderTest.updateDmiRegistration(dmiPluginRegistration)
183         then: 'a DMI Request Exception is thrown with correct message details'
184             def exceptionThrown = thrown(DmiRequestException.class)
185             assert exceptionThrown.getMessage().contains(expectedMessageDetails)
186         and: 'registration is not called'
187             0 * objectUnderTest.processCreatedCmHandles(*_)
188         where:
189             scenario                         | dmiPlugin  | dmiModelPlugin | dmiDataPlugin || expectedMessageDetails
190             'empty DMI plugins'              | ''         | ''             | ''            || 'No DMI plugin service names'
191             'blank DMI plugins'              | ' '        | ' '            | ' '           || 'No DMI plugin service names'
192             'null DMI plugins'               | null       | null           | null          || 'No DMI plugin service names'
193             'all DMI plugins'                | 'service1' | 'service2'     | 'service3'    || 'Cannot register combined plugin service name and other service names'
194             '(combined)DMI and Data Plugin'  | 'service1' | ''             | 'service2'    || 'Cannot register combined plugin service name and other service names'
195             '(combined)DMI and model Plugin' | 'service1' | 'service2'     | ''            || 'Cannot register combined plugin service name and other service names'
196             'only model DMI plugin'          | ''         | 'service1'     | ''            || 'Cannot register just a Data or Model plugin service name'
197             'only data DMI plugin'           | ''         | ''             | 'service1'    || 'Cannot register just a Data or Model plugin service name'
198     }
199
200     def 'Create CM-Handle Successfully: #scenario.'() {
201         given: 'a registration without cm-handle properties'
202             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
203             dmiPluginRegistration.createdCmHandles = [new NcmpServiceCmHandle(cmHandleId: 'cmhandle', dmiProperties: dmiProperties, publicProperties: publicProperties)]
204         when: 'registration is updated'
205             def response = objectUnderTest.updateDmiRegistration(dmiPluginRegistration)
206         then: 'a successful response is received'
207             response.createdCmHandles.size() == 1
208             with(response.createdCmHandles[0]) {
209                 assert it.status == Status.SUCCESS
210                 assert it.cmHandle == 'cmhandle'
211             }
212         and: 'state handler is invoked with the expected parameters'
213             1 * mockLcmEventsCmHandleStateHandler.initiateStateAdvised(_) >> {
214                 args ->  {
215                         def yangModelCmHandles = args[0]
216                         assert yangModelCmHandles.id == ['cmhandle']
217                         assert yangModelCmHandles.dmiServiceName == ['my-server']
218                     }
219             }
220         where:
221             scenario                          | dmiProperties            | publicProperties               || expectedDmiProperties                      | expectedPublicProperties
222             'with dmi & public properties'    | ['dmi-key': 'dmi-value'] | ['public-key': 'public-value'] || '[{"name":"dmi-key","value":"dmi-value"}]' | '[{"name":"public-key","value":"public-value"}]'
223             'with only public properties'     | [:]                      | ['public-key': 'public-value'] || [:]                                        | '[{"name":"public-key","value":"public-value"}]'
224             'with only dmi properties'        | ['dmi-key': 'dmi-value'] | [:]                            || '[{"name":"dmi-key","value":"dmi-value"}]' | [:]
225             'without dmi & public properties' | [:]                      | [:]                            || [:]                                        | [:]
226     }
227
228     def 'Add CM-Handle #scenario.'() {
229         given: ' registration details for one cm handles'
230             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
231                 createdCmHandles:[new NcmpServiceCmHandle(cmHandleId: 'ch-1', registrationTrustLevel: registrationTrustLevel)])
232         when: 'registration is updated'
233             objectUnderTest.updateDmiRegistration(dmiPluginRegistration)
234         then: 'trustLevel is set for the created cm-handle'
235             1 * mockTrustLevelManager.registerCmHandles(expectedMapping)
236         where:
237             scenario                 | registrationTrustLevel || expectedMapping
238             'with trusted cm handle' | TrustLevel.COMPLETE    || [ 'ch-1' : TrustLevel.COMPLETE ]
239             'without trust level'    | null                   || [ 'ch-1' : null ]
240     }
241
242     def 'Create CM-Handle Multiple Requests: All cm-handles creation requests are processed with some failures'() {
243         given: 'a registration with three cm-handles to be created'
244             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
245                     createdCmHandles: [new NcmpServiceCmHandle(cmHandleId: 'cmhandle1'),
246                                        new NcmpServiceCmHandle(cmHandleId: 'cmhandle2'),
247                                        new NcmpServiceCmHandle(cmHandleId: 'cmhandle3')])
248         and: 'cm-handle creation is successful for 1st and 3rd; failed for 2nd'
249             def xpath = "somePathWithId[@id='cmhandle2']"
250             mockLcmEventsCmHandleStateHandler.initiateStateAdvised(*_) >> { throw AlreadyDefinedException.forDataNodes([xpath], 'some-context') }
251         when: 'registration is updated to create cm-handles'
252             def response = objectUnderTest.updateDmiRegistration(dmiPluginRegistration)
253         then: 'a response is received for all cm-handles'
254             response.createdCmHandles.size() == 1
255         and: 'all cm-handles creation fails'
256             response.createdCmHandles.each {
257                 assert it.cmHandle == 'cmhandle2'
258                 assert it.status == Status.FAILURE
259                 assert it.ncmpResponseStatus == CM_HANDLE_ALREADY_EXIST
260                 assert it.errorText == 'cm-handle already exists'
261             }
262     }
263
264     def 'Create CM-Handle Error Handling: Registration fails: #scenario'() {
265         given: 'a registration without cm-handle properties'
266             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
267             dmiPluginRegistration.createdCmHandles = [new NcmpServiceCmHandle(cmHandleId: 'cmhandle')]
268         and: 'cm-handler registration fails: #scenario'
269             mockLcmEventsCmHandleStateHandler.initiateStateAdvised(*_) >> { throw exception }
270         when: 'registration is updated'
271             def response = objectUnderTest.updateDmiRegistration(dmiPluginRegistration)
272         then: 'a failure response is received'
273             response.createdCmHandles.size() == 1
274             with(response.createdCmHandles[0]) {
275                 assert it.status == Status.FAILURE
276                 assert it.cmHandle ==  'cmhandle'
277                 assert it.ncmpResponseStatus == expectedError
278                 assert it.errorText == expectedErrorText
279             }
280         where:
281             scenario                                        | exception                                                                      || expectedError           | expectedErrorText
282             'cm-handle already exist'                       | AlreadyDefinedException.forDataNodes(["path[@id='cmhandle']"], 'some-context') || CM_HANDLE_ALREADY_EXIST | 'cm-handle already exists'
283             'unknown exception while registering cm-handle' | new RuntimeException('Failed')                                                 || UNKNOWN_ERROR           | 'Failed'
284     }
285
286     def 'Update CM-Handle: Update Operation Response is added to the response'() {
287         given: 'a registration to update CmHandles'
288             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server', updatedCmHandles: [{}])
289         and: 'cm-handle updates can be processed successfully'
290             def updateOperationResponse = [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-1'),
291                                            CmHandleRegistrationResponse.createFailureResponse('cm-handle-2', new Exception("Failed")),
292                                            CmHandleRegistrationResponse.createFailureResponse('cm-handle-3', CM_HANDLES_NOT_FOUND),
293                                            CmHandleRegistrationResponse.createFailureResponse('cm handle 4', CM_HANDLE_INVALID_ID)]
294             mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(_) >> updateOperationResponse
295         when: 'registration is updated'
296             def response = objectUnderTest.updateDmiRegistration(dmiPluginRegistration)
297         then: 'the response contains updateOperationResponse'
298             assert response.updatedCmHandles.size() == 4
299             assert response.updatedCmHandles.containsAll(updateOperationResponse)
300     }
301
302     def 'Remove CmHandle Successfully'() {
303         given: 'a registration update to delete a cm handle'
304             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server', removedCmHandles: ['cmhandle'])
305         when: 'the registration is updated'
306             def response = objectUnderTest.updateDmiRegistration(dmiPluginRegistration)
307         then: 'the cmHandle state is set to "DELETING"'
308             1 * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(_) >> { args -> args[0].values()[0] == CmHandleState.DELETING }
309         then: 'method to delete anchors is called once'
310             1 * mockInventoryPersistence.deleteAnchors(_)
311         and: 'method to delete relevant list/list element is called once'
312             1 * mockInventoryPersistence.deleteDataNodes(_)
313         and: 'successful response is received'
314             assert response.removedCmHandles.size() == 1
315             with(response.removedCmHandles[0]) {
316                 assert it.status == Status.SUCCESS
317                 assert it.cmHandle == 'cmhandle'
318             }
319         and: 'the cmHandle state is updated to "DELETED"'
320             1 * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(_) >>  { args -> args[0].values()[0] == CmHandleState.DELETED }
321     }
322
323     def 'Remove CmHandle: Partial Success'() {
324         given: 'a registration with three cm-handles to be deleted'
325             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
326                 removedCmHandles: ['cmhandle1', 'cmhandle2', 'cmhandle3'])
327         and: 'cm handles to be deleted in the progress map'
328             mockModuleSyncStartedOnCmHandles.containsKey("cmhandle1") >> true
329             mockModuleSyncStartedOnCmHandles.containsKey("cmhandle3") >> true
330         and: 'delete fails for batch. Retry only fails for and cm handle 2'
331             mockInventoryPersistence.deleteDataNodes(_) >> { throw new RuntimeException("Batch Failed") }
332                                                         >> { /* cm handle 1 is OK */ }
333                                                         >> { throw new RuntimeException("Cm handle 2 Failed")}
334                                                         >> { /* cm handle 3 is OK */ }
335         when: 'registration is updated to delete cmhandles'
336             def response = objectUnderTest.updateDmiRegistration(dmiPluginRegistration)
337         then: 'the cmHandle states are all updated to "DELETING"'
338             1 * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch({ assert it.every { entry -> entry.value == CmHandleState.DELETING } })
339         and: 'a response is received for all cm-handles'
340             response.removedCmHandles.size() == 3
341         and: 'successfully de-registered cm handle 1 is removed from in progress map'
342             1 * mockModuleSyncStartedOnCmHandles.removeAsync('cmhandle1')
343         and: 'successfully de-registered cm handle 3 is removed from in progress map even though it was already being removed'
344             1 * mockModuleSyncStartedOnCmHandles.removeAsync('cmhandle3')
345         and: 'failed de-registered cm handle entries should NOT be removed from in progress map'
346             0 * mockModuleSyncStartedOnCmHandles.removeAsync('cmhandle2')
347         and: '1st and 3rd cm-handle deletes successfully'
348             with(response.removedCmHandles[0]) {
349                 assert it.status == Status.SUCCESS
350                 assert it.cmHandle == 'cmhandle1'
351             }
352             with(response.removedCmHandles[2]) {
353                 assert it.status == Status.SUCCESS
354                 assert it.cmHandle == 'cmhandle3'
355             }
356         and: '2nd cm-handle deletion fails'
357             with(response.removedCmHandles[1]) {
358                 assert it.status == Status.FAILURE
359                 assert it.ncmpResponseStatus == UNKNOWN_ERROR
360                 assert it.errorText == 'Cm handle 2 Failed'
361                 assert it.cmHandle == 'cmhandle2'
362             }
363         and: 'the cmHandle state is updated to DELETED for 1st and 3rd'
364             1 * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch({
365                 assert it.size() == 2
366                 assert it.every { entry -> entry.value == CmHandleState.DELETED }
367             })
368     }
369
370     def 'Remove CmHandle Error Handling: #scenario'() {
371         given: 'a registration'
372             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server', removedCmHandles: ['cmhandle'])
373         and: 'cm-handle deletion fails on batch'
374             mockInventoryPersistence.deleteDataNodes(_) >> { throw deleteListElementException }
375         and: 'cm-handle deletion fails on individual delete'
376             mockInventoryPersistence.deleteDataNode(_) >> { throw deleteListElementException }
377         when: 'registration is updated to delete cmhandle'
378             def response = objectUnderTest.updateDmiRegistration(dmiPluginRegistration)
379         then: 'a failure response is received'
380             assert response.removedCmHandles.size() == 1
381             with(response.removedCmHandles[0]) {
382                 assert it.status == Status.FAILURE
383                 assert it.cmHandle == 'cmhandle'
384                 assert it.ncmpResponseStatus == expectedError
385                 assert it.errorText == expectedErrorText
386             }
387         and: 'the cm handle state is not updated to "DELETED"'
388             0 * mockLcmEventsCmHandleStateHandler.updateCmHandleStateBatch(_, CmHandleState.DELETED)
389         where:
390         scenario                     | deleteListElementException                || expectedError        | expectedErrorText
391         'cm-handle does not exist'   | new DataNodeNotFoundException('', '', '') || CM_HANDLES_NOT_FOUND | 'cm handle reference(s) not found'
392         'cm-handle has invalid name' | new DataValidationException('', '')       || CM_HANDLE_INVALID_ID | 'cm handle reference has an invalid character(s) in id'
393         'an unexpected exception'    | new RuntimeException('Failed')            || UNKNOWN_ERROR        | 'Failed'
394     }
395
396     def 'Set Cm Handle Data Sync Enabled Flag where data sync flag is #scenario'() {
397         given: 'an existing cm handle composite state'
398             def compositeState = new CompositeState(cmHandleState: CmHandleState.READY, dataSyncEnabled: initialDataSyncEnabledFlag,
399                 dataStores: CompositeState.DataStores.builder()
400                     .operationalDataStore(CompositeState.Operational.builder()
401                         .dataStoreSyncState(initialDataSyncState)
402                         .build()).build())
403         and: 'get cm handle state returns the composite state for the given cm handle id'
404             mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
405         when: 'set data sync enabled is called with the data sync enabled flag set to #dataSyncEnabledFlag'
406             objectUnderTest.setDataSyncEnabled('some-cm-handle-id', dataSyncEnabledFlag)
407         then: 'the data sync enabled flag is set to #dataSyncEnabled'
408             compositeState.dataSyncEnabled == dataSyncEnabledFlag
409         and: 'the data store sync state is set to #expectedDataStoreSyncState'
410             compositeState.dataStores.operationalDataStore.dataStoreSyncState == expectedDataStoreSyncState
411         and: 'the cps data service to delete data nodes is invoked the expected number of times'
412             deleteDataNodeExpectedNumberOfInvocation * mockCpsDataService.deleteDataNode(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'some-cm-handle-id', '/netconf-state', _)
413         and: 'the inventory persistence service to update node leaves is called with the correct values'
414             saveCmHandleStateExpectedNumberOfInvocations * mockInventoryPersistence.saveCmHandleState('some-cm-handle-id', compositeState)
415         where: 'the following data sync enabled flag is used'
416             scenario                                              | dataSyncEnabledFlag | initialDataSyncEnabledFlag | initialDataSyncState               || expectedDataStoreSyncState         | deleteDataNodeExpectedNumberOfInvocation | saveCmHandleStateExpectedNumberOfInvocations
417             'enabled'                                             | true                | false                      | DataStoreSyncState.NONE_REQUESTED  || DataStoreSyncState.UNSYNCHRONIZED  | 0                                        | 1
418             'disabled'                                            | false               | true                       | DataStoreSyncState.UNSYNCHRONIZED  || DataStoreSyncState.NONE_REQUESTED  | 0                                        | 1
419             'disabled where sync-state is currently SYNCHRONIZED' | false               | true                       | DataStoreSyncState.SYNCHRONIZED    || DataStoreSyncState.NONE_REQUESTED  | 1                                        | 1
420             'is set to existing flag state'                       | true                | true                       | DataStoreSyncState.UNSYNCHRONIZED  || DataStoreSyncState.UNSYNCHRONIZED  | 0                                        | 0
421     }
422
423     def 'Set cm Handle Data Sync Enabled flag with following cm handle not in ready state exception' () {
424         given: 'a cm handle composite state'
425             def compositeState = new CompositeState(cmHandleState: CmHandleState.ADVISED, dataSyncEnabled: false)
426         and: 'get cm handle state returns the composite state for the given cm handle id'
427             mockInventoryPersistence.getCmHandleState('some-cm-handle-id') >> compositeState
428         when: 'set data sync enabled is called with the data sync enabled flag set to true'
429             objectUnderTest.setDataSyncEnabled('some-cm-handle-id', true)
430         then: 'the expected exception is thrown'
431             thrown(CpsException)
432         and: 'the inventory persistence service to update node leaves is not invoked'
433             0 * mockInventoryPersistence.saveCmHandleState(_, _)
434     }
435
436 }