b806258008e1a4052cf417550b3d2bda71fed6c4
[cps.git] /
1 /*
2  * ============LICENSE_START=======================================================
3  * Copyright (c) 2024-2025 OpenInfra Foundation Europe. All rights reserved.
4  *  ================================================================================
5  *  Licensed under the Apache License, Version 2.0 (the "License");
6  *  you may not use this file except in compliance with the License.
7  *  You may obtain a copy of the License at
8  *
9  *        http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an 'AS IS' BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  *
17  *  SPDX-License-Identifier: Apache-2.0
18  *  ============LICENSE_END=========================================================
19  */
20
21 package org.onap.cps.ncmp.impl.datajobs.subscription.ncmp
22
23 import static org.onap.cps.ncmp.impl.datajobs.subscription.models.CmSubscriptionStatus.ACCEPTED
24 import static org.onap.cps.ncmp.impl.datajobs.subscription.models.CmSubscriptionStatus.REJECTED
25
26 import org.onap.cps.ncmp.impl.datajobs.subscription.client_to_ncmp.DataSelector
27 import org.onap.cps.ncmp.impl.datajobs.subscription.dmi.DmiInEventMapper
28 import org.onap.cps.ncmp.impl.datajobs.subscription.dmi.EventProducer
29 import org.onap.cps.ncmp.impl.datajobs.subscription.ncmp_to_dmi.DataJobSubscriptionDmiInEvent
30 import org.onap.cps.ncmp.impl.datajobs.subscription.utils.CmDataJobSubscriptionPersistenceService
31 import org.onap.cps.ncmp.impl.inventory.InventoryPersistence
32 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
33 import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher
34 import org.onap.cps.ncmp.impl.utils.JexParser
35 import spock.lang.Specification
36
37 class CmSubscriptionHandlerImplSpec extends Specification {
38
39     def mockCmSubscriptionPersistenceService = Mock(CmDataJobSubscriptionPersistenceService)
40     def mockDmiInEventMapper = Mock(DmiInEventMapper)
41     def mockDmiInEventProducer = Mock(EventProducer)
42     def mockInventoryPersistence = Mock(InventoryPersistence)
43     def mockAlternateIdMatcher = Mock(AlternateIdMatcher)
44
45     void setup() {
46         mockCmSubscriptionPersistenceService.isNewSubscriptionId(!'existingId') >> true
47     }
48
49     def objectUnderTest = new CmSubscriptionHandlerImpl(mockCmSubscriptionPersistenceService, mockDmiInEventMapper,
50             mockDmiInEventProducer, mockInventoryPersistence, mockAlternateIdMatcher)
51
52     def 'Attempt to create already existing subscription.'() {
53         given: 'the persistence service indicates the id is not new'
54             mockCmSubscriptionPersistenceService.isNewSubscriptionId('existingId') >> false
55         when: 'attempt to create the subscription'
56             objectUnderTest.createSubscription(new DataSelector(), 'existingId', ['/someDataNodeSelector'])
57         then: 'request is ignored and no method is invoked'
58             0 * mockCmSubscriptionPersistenceService.add(*_)
59         and: 'no events are sent'
60             0 * mockDmiInEventProducer.send(*_)
61     }
62
63     def 'Process subscription CREATE request for new target [non existing]'() {
64         given: 'relevant subscription details'
65             def mySubId = 'dataJobId'
66             def myDataNodeSelectors = ['/parent[id="1"]'].toList()
67             def notificationTypes = []
68             def notificationFilter = ''
69             def dataSelector = new DataSelector(notificationTypes: notificationTypes, notificationFilter: notificationFilter)
70         and: 'alternate Id matcher returns cm handle id for given data node selector'
71             def fdn = getFdn(myDataNodeSelectors.iterator().next())
72             mockAlternateIdMatcher.getCmHandleId(fdn) >> 'myCmHandleId'
73         and: 'the persistence service returns inactive data node selector(s)'
74             mockCmSubscriptionPersistenceService.getInactiveDataNodeSelectors(mySubId) >> ['/parent[id="1"]']
75         and: 'the inventory persistence service returns cm handle'
76             mockInventoryPersistence.getYangModelCmHandle('myCmHandleId') >> new YangModelCmHandle(dmiServiceName: 'myDmiService')
77         and: 'DMI in event mapper returns event'
78             def myDmiInEvent = new DataJobSubscriptionDmiInEvent()
79             mockDmiInEventMapper.toDmiInEvent(['myCmHandleId'], myDataNodeSelectors, notificationTypes, notificationFilter) >> myDmiInEvent
80         when: 'a subscription is created'
81             objectUnderTest.createSubscription(dataSelector, mySubId, myDataNodeSelectors)
82         then: 'each datanode selector is added using the persistence service'
83             1 * mockCmSubscriptionPersistenceService.add(mySubId, '/parent[id="1"]')
84         and: 'an event is sent to the correct DMI'
85             1 * mockDmiInEventProducer.send(mySubId, 'myDmiService', 'subscriptionCreateRequest', _)
86     }
87
88     def 'Process subscription CREATE request for new targets [non existing] to be sent to multiple DMIs'() {
89         given: 'relevant subscription details'
90             def mySubId = 'dataJobId'
91             def myDataNodeSelectors = [
92                     '/parent[id="forDmi1"]',
93                     '/parent[id="forDmi1"]/child',
94                     '/parent[id="forDmi2"]'].toList()
95             def notificationTypes = []
96             def notificationFilter = ''
97             def dataSelector = new DataSelector(notificationTypes: notificationTypes, notificationFilter: notificationFilter)
98         and: 'alternate Id matcher returns cm handle ids for given data node selectors'
99             def fdn1 = getFdn(myDataNodeSelectors[0])
100             def fdn2 = getFdn(myDataNodeSelectors[1])
101             def fdn3 = getFdn(myDataNodeSelectors[2])
102             mockAlternateIdMatcher.getCmHandleId(fdn1) >> 'myCmHandleId1'
103             mockAlternateIdMatcher.getCmHandleId(fdn2) >> 'myCmHandleId1'
104             mockAlternateIdMatcher.getCmHandleId(fdn3) >> 'myCmHandleId2'
105         and: 'the persistence service returns inactive data node selector(s)'
106             mockCmSubscriptionPersistenceService.getInactiveDataNodeSelectors(mySubId) >> [
107                     '/parent[id="forDmi1"]',
108                     '/parent[id="forDmi1"]/child',
109                     '/parent[id="forDmi2"]']
110         and: 'the inventory persistence service returns cm handles with dmi information'
111             mockInventoryPersistence.getYangModelCmHandle('myCmHandleId1') >> new YangModelCmHandle(dmiServiceName: 'myDmiService1')
112             mockInventoryPersistence.getYangModelCmHandle('myCmHandleId2') >> new YangModelCmHandle(dmiServiceName: 'myDmiService2')
113         and: 'DMI in event mapper returns events'
114             def myDmiInEvent1 = new DataJobSubscriptionDmiInEvent()
115             def myDmiInEvent2 = new DataJobSubscriptionDmiInEvent()
116             mockDmiInEventMapper.toDmiInEvent(['myCmHandleId1'], ['/parent[id="forDmi1"]', '/parent[id="forDmi1"]/child'], notificationTypes, notificationFilter) >> myDmiInEvent1
117             mockDmiInEventMapper.toDmiInEvent(['myCmHandleId2'], ['/parent[id="forDmi2"]'], notificationTypes, notificationFilter) >> myDmiInEvent2
118         when: 'a subscription is created'
119             objectUnderTest.createSubscription(dataSelector, mySubId, myDataNodeSelectors)
120         then: 'each datanode selector is added using the persistence service'
121             myDataNodeSelectors.each { dataNodeSelector ->
122                 1 * mockCmSubscriptionPersistenceService.add(_, dataNodeSelector)
123             }
124         and: 'an event is sent to each DMI involved'
125             1 * mockDmiInEventProducer.send(mySubId, 'myDmiService1', 'subscriptionCreateRequest', myDmiInEvent1)
126             1 * mockDmiInEventProducer.send(mySubId, 'myDmiService2', 'subscriptionCreateRequest', myDmiInEvent2)
127     }
128
129     def 'Process subscription CREATE request for overlapping targets [non existing & existing]'() {
130         given: 'relevant subscription details'
131             def myNewSubId = 'newId'
132             def myDataNodeSelectors = ['/newDataNodeSelector[id=""]'].toList()
133             def dataSelector = new DataSelector(notificationTypes: [], notificationFilter: '')
134         and: 'alternate id matcher always returns a cm handle id'
135             mockAlternateIdMatcher.getCmHandleId(_) >> 'someCmHandleId'
136         and: 'the inventory persistence service returns cm handles with dmi information'
137             mockInventoryPersistence.getYangModelCmHandle(_) >> new YangModelCmHandle(dmiServiceName: 'myDmiService')
138         and: 'the inventory persistence service returns inactive data node selector(s)'
139             mockCmSubscriptionPersistenceService.getInactiveDataNodeSelectors(myNewSubId) >> inactiveDataNodeSelectors
140         when: 'a subscription is created'
141             objectUnderTest.createSubscription(dataSelector, myNewSubId, myDataNodeSelectors)
142         then: 'each datanode selector is added using the persistence service'
143             1 * mockCmSubscriptionPersistenceService.add(_, myDataNodeSelectors.iterator().next())
144         and: 'an event is sent to each DMI involved'
145             expectedCallsToDmi * mockDmiInEventProducer.send(myNewSubId, 'myDmiService', 'subscriptionCreateRequest', _)
146         where: 'following data are used'
147             scenario                                            | inactiveDataNodeSelectors                                           || expectedCallsToDmi
148             'new target overlaps with ACCEPTED targets'         | []                                                                  || 0
149             'new target overlaps with REJECTED targets'         | ['/existingDataNodeSelector[id=""]', '/newDataNodeSelector[id=""]'] || 1
150             'new target overlaps with UNKNOWN targets'          | ['/existingDataNodeSelector[id=""]', '/newDataNodeSelector[id=""]'] || 1
151             'new target does not overlap with existing targets' | ['/newDataNodeSelector[id=""]']                                     || 1
152     }
153
154     def 'Process subscription DELETE request where all data node selectors become unused'() {
155         given: 'a subscription id and its associated data node selectors'
156             def mySubId = 'deleteJobId'
157             def myDataNodeSelector = ['/node[id="1"]']
158         and: 'the persistence service returns the data node selectors'
159             mockCmSubscriptionPersistenceService.getDataNodeSelectors(mySubId) >> myDataNodeSelector
160         and: 'no other subscriptions exist for the data node selectors'
161             mockCmSubscriptionPersistenceService.getSubscriptionIds('/node[id="1"]') >> []
162         and: 'cm handle resolution setup'
163             def fdn = getFdn('/node[id="1"]')
164             mockAlternateIdMatcher.getCmHandleId(fdn) >> 'cmHandleId1'
165             mockInventoryPersistence.getYangModelCmHandle('cmHandleId1') >> new YangModelCmHandle(dmiServiceName: 'dmiService1')
166         and: 'DMI in event mapper returns events'
167             def deleteEvent = new DataJobSubscriptionDmiInEvent()
168             mockDmiInEventMapper.toDmiInEvent(['cmHandleId1'], ['/node[id="1"]'], null, null) >> deleteEvent
169         when: 'a subscription is deleted'
170             objectUnderTest.deleteSubscription(mySubId)
171         then: 'subscription is removed from persistence'
172             1 * mockCmSubscriptionPersistenceService.delete(mySubId, '/node[id="1"]')
173         and: 'an event is sent to each DMI involved'
174             1 * mockDmiInEventProducer.send(mySubId, 'dmiService1', 'subscriptionDeleteRequest', deleteEvent)
175     }
176
177     def 'Process subscription DELETE request where some data node selectors are still in use'() {
178         given: 'a subscription id and two associated selectors'
179             def mySubId = 'deleteJobId2'
180             def dataNodeSelectors = ['/node[id="1"]', '/node[id="2"]']
181         and: 'the persistence service returns the data node selectors'
182             mockCmSubscriptionPersistenceService.getDataNodeSelectors(mySubId) >> dataNodeSelectors
183         and: 'data node selector 1 has no more subscribers, data node selector 2 still has subscribers'
184             mockCmSubscriptionPersistenceService.getSubscriptionIds('/node[id="1"]') >> []
185             mockCmSubscriptionPersistenceService.getSubscriptionIds('/node[id="2"]') >> ['anotherSub']
186         and: 'cm handle resolution for data node selector 1'
187             def fdn = getFdn('/node[id="1"]')
188             mockAlternateIdMatcher.getCmHandleId(fdn) >> 'cmHandleIdX'
189             mockInventoryPersistence.getYangModelCmHandle('cmHandleIdX') >> new YangModelCmHandle(dmiServiceName: 'dmiServiceX')
190         and: 'DMI in event mapper returns events'
191             def deleteEvent = new DataJobSubscriptionDmiInEvent()
192             mockDmiInEventMapper.toDmiInEvent(['cmHandleIdX'], ['/node[id="1"]'], null, null) >> deleteEvent
193         when: 'a subscription is deleted'
194             objectUnderTest.deleteSubscription(mySubId)
195         then: 'subscription is removed from persistence for both data node selectors'
196             1 * mockCmSubscriptionPersistenceService.delete(mySubId, '/node[id="1"]')
197             1 * mockCmSubscriptionPersistenceService.delete(mySubId, '/node[id="2"]')
198         and: 'delete event is sent only for data node selectors without any subscriber'
199             1 * mockDmiInEventProducer.send(mySubId, 'dmiServiceX', 'subscriptionDeleteRequest', deleteEvent)
200     }
201
202     def 'Process subscription DELETE request where cmHandleId cannot be resolved'() {
203         given: 'a subscription id and its data node selector'
204             def mySubId = 'deleteJobId3'
205             def dataNodeSelectors = ['/node[id="unresolvable"]']
206         and: 'the persistence service returns the data node selectors'
207             mockCmSubscriptionPersistenceService.getDataNodeSelectors(mySubId) >> dataNodeSelectors
208         and: 'no more subscriptions exist for the data node selector'
209             mockCmSubscriptionPersistenceService.getSubscriptionIds('/node[id="unresolvable"]') >> []
210         and: 'alternate id matcher cannot resolve cm handle id'
211             def fdn = getFdn('/node[id="unresolvable"]')
212             mockAlternateIdMatcher.getCmHandleId(fdn) >> null
213         and: 'DMI in event mapper returns events'
214             def deleteEvent = new DataJobSubscriptionDmiInEvent()
215             mockDmiInEventMapper.toDmiInEvent(['cmHandleIdX'], ['/node[id="1"]'], null, null) >> deleteEvent
216         when: 'a subscription is deleted'
217             objectUnderTest.deleteSubscription(mySubId)
218         then: 'subscription is removed from persistence'
219             1 * mockCmSubscriptionPersistenceService.delete(mySubId, '/node[id="unresolvable"]')
220         and: 'no delete event is sent because cmHandleId was not resolved'
221             0 * mockDmiInEventProducer.send(*_)
222     }
223
224     def 'Update subscription status to ACCEPTED: #scenario'() {
225         given: 'a subscription ID'
226             def mySubscriptionId = 'mySubId'
227         and: 'the persistence service returns all inactive data node selectors'
228             def myDataNodeSelectors = ['/myDataNodeSelector[id=""]'].asList()
229             mockCmSubscriptionPersistenceService.getInactiveDataNodeSelectors(mySubscriptionId) >> myDataNodeSelectors
230         and: 'alternate id matcher always returns a cm handle id'
231             mockAlternateIdMatcher.getCmHandleId(_) >> 'someCmHandleId'
232         and: 'the inventory persistence service returns a yang model with a dmi service name for the accepted subscription'
233             mockInventoryPersistence.getYangModelCmHandle(_) >> new YangModelCmHandle(dmiServiceName: 'myDmi')
234         when: 'the method to update subscription status is called with status=ACCEPTED and dmi #dmiName'
235             objectUnderTest.updateCmSubscriptionStatus(mySubscriptionId, dmiName, ACCEPTED)
236         then: 'the persistence service to update subscription status is ONLY called for matching dmi name'
237             expectedCallsToPersistenceService * mockCmSubscriptionPersistenceService.updateCmSubscriptionStatus('/myDataNodeSelector[id=""]', ACCEPTED)
238         where: 'the following data are used'
239             scenario                           | dmiName        || expectedCallsToPersistenceService
240             'data node selector for "myDmi"'   | 'myDmi'        || 1
241             'data node selector for other dmi' | 'someOtherDmi' || 0
242     }
243
244     def 'Log update when subscription status is REJECTED'() {
245         given: 'dmi service name and subscription id'
246             def myDmi = 'myDmi'
247             def mySubscriptionId = 'mySubscriptionId'
248         and: 'the persistence service returns all inactive data node selectors'
249             def myDataNodeSelectors = ['/parent[id=""]'].asList()
250             mockCmSubscriptionPersistenceService.getInactiveDataNodeSelectors(mySubscriptionId) >> myDataNodeSelectors
251         and: 'alternate id matcher always returns a cm handle id'
252             mockAlternateIdMatcher.getCmHandleId(_) >> 'someCmHandleId'
253         and: 'the inventory persistence service returns a yang model with the given dmi service name'
254             mockInventoryPersistence.getYangModelCmHandle(_) >> new YangModelCmHandle(dmiServiceName: myDmi)
255         when: 'update subscription status is called with status=REJECTED'
256             objectUnderTest.updateCmSubscriptionStatus(mySubscriptionId, myDmi, REJECTED)
257         then: 'the persistence service to update subscription status called with REJECTED for matching dmi name'
258             1 * mockCmSubscriptionPersistenceService.updateCmSubscriptionStatus('/parent[id=""]', REJECTED)
259     }
260
261
262     def getFdn(dataNodeSelector) {
263         return JexParser.extractFdnPrefix(dataNodeSelector).orElse("")
264     }
265 }