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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 * SPDX-License-Identifier: Apache-2.0
18 * ============LICENSE_END=========================================================
21 package org.onap.cps.ncmp.impl.datajobs.subscription.ncmp
23 import org.onap.cps.ncmp.impl.datajobs.subscription.client_to_ncmp.DataSelector
24 import org.onap.cps.ncmp.impl.datajobs.subscription.dmi.DmiInEventMapper
25 import org.onap.cps.ncmp.impl.datajobs.subscription.dmi.EventProducer
26 import org.onap.cps.ncmp.impl.datajobs.subscription.ncmp_to_dmi.DataJobSubscriptionDmiInEvent
27 import org.onap.cps.ncmp.impl.datajobs.subscription.utils.CmDataJobSubscriptionPersistenceService
28 import org.onap.cps.ncmp.impl.inventory.InventoryPersistence
29 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
30 import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher
31 import org.onap.cps.ncmp.impl.utils.JexParser
32 import spock.lang.Specification
34 import static org.onap.cps.ncmp.impl.datajobs.subscription.models.CmSubscriptionStatus.ACCEPTED
36 class CmSubscriptionHandlerImplSpec extends Specification {
38 def mockCmSubscriptionPersistenceService = Mock(CmDataJobSubscriptionPersistenceService)
39 def mockDmiInEventMapper = Mock(DmiInEventMapper)
40 def mockDmiInEventProducer = Mock(EventProducer)
41 def mockInventoryPersistence = Mock(InventoryPersistence)
42 def mockAlternateIdMatcher = Mock(AlternateIdMatcher)
44 def objectUnderTest = new CmSubscriptionHandlerImpl(mockCmSubscriptionPersistenceService, mockDmiInEventMapper,
45 mockDmiInEventProducer, mockInventoryPersistence, mockAlternateIdMatcher)
47 def 'Process subscription CREATE request for new target [non existing]'() {
48 given: 'relevant subscription details'
49 def mySubId = 'dataJobId'
50 def myDataNodeSelectors = ['/parent[id="1"]'].toList()
51 def notificationTypes = []
52 def notificationFilter = ''
53 def dataSelector = new DataSelector(notificationTypes: notificationTypes, notificationFilter: notificationFilter)
54 and: 'alternate Id matcher returns cm handle id for given data node selector'
55 def fdn = getFdn(myDataNodeSelectors.iterator().next())
56 mockAlternateIdMatcher.getCmHandleId(fdn) >> 'myCmHandleId'
57 and: 'the persistence service returns inactive data node selector(s)'
58 mockCmSubscriptionPersistenceService.getInactiveDataNodeSelectors(mySubId) >> ['/parent[id="1"]']
59 and: 'the inventory persistence service returns cm handle'
60 mockInventoryPersistence.getYangModelCmHandle('myCmHandleId') >> new YangModelCmHandle(dmiServiceName: 'myDmiService')
61 and: 'DMI in event mapper returns event'
62 def myDmiInEvent = new DataJobSubscriptionDmiInEvent()
63 mockDmiInEventMapper.toDmiInEvent(['myCmHandleId'], myDataNodeSelectors, notificationTypes, notificationFilter) >> myDmiInEvent
64 when: 'a subscription is created'
65 objectUnderTest.createSubscription(dataSelector, mySubId, myDataNodeSelectors)
66 then: 'each datanode selector is added using the persistence service'
67 1 * mockCmSubscriptionPersistenceService.add(mySubId, '/parent[id="1"]')
68 and: 'an event is sent to the correct DMI'
69 1 * mockDmiInEventProducer.send(mySubId, 'myDmiService', 'subscriptionCreateRequest', _)
72 def 'Process subscription CREATE request for new targets [non existing] to be sent to multiple DMIs'() {
73 given: 'relevant subscription details'
74 def mySubId = 'dataJobId'
75 def myDataNodeSelectors = [
76 '/parent[id="forDmi1"]',
77 '/parent[id="forDmi1"]/child',
78 '/parent[id="forDmi2"]'].toList()
79 def notificationTypes = []
80 def notificationFilter = ''
81 def dataSelector = new DataSelector(notificationTypes: notificationTypes, notificationFilter: notificationFilter)
82 and: 'alternate Id matcher returns cm handle ids for given data node selectors'
83 def fdn1 = getFdn(myDataNodeSelectors[0])
84 def fdn2 = getFdn(myDataNodeSelectors[1])
85 def fdn3 = getFdn(myDataNodeSelectors[2])
86 mockAlternateIdMatcher.getCmHandleId(fdn1) >> 'myCmHandleId1'
87 mockAlternateIdMatcher.getCmHandleId(fdn2) >> 'myCmHandleId1'
88 mockAlternateIdMatcher.getCmHandleId(fdn3) >> 'myCmHandleId2'
89 and: 'the persistence service returns inactive data node selector(s)'
90 mockCmSubscriptionPersistenceService.getInactiveDataNodeSelectors(mySubId) >> [
91 '/parent[id="forDmi1"]',
92 '/parent[id="forDmi1"]/child',
93 '/parent[id="forDmi2"]']
94 and: 'the inventory persistence service returns cm handles with dmi information'
95 mockInventoryPersistence.getYangModelCmHandle('myCmHandleId1') >> new YangModelCmHandle(dmiServiceName: 'myDmiService1')
96 mockInventoryPersistence.getYangModelCmHandle('myCmHandleId2') >> new YangModelCmHandle(dmiServiceName: 'myDmiService2')
97 and: 'DMI in event mapper returns events'
98 def myDmiInEvent1 = new DataJobSubscriptionDmiInEvent()
99 def myDmiInEvent2 = new DataJobSubscriptionDmiInEvent()
100 mockDmiInEventMapper.toDmiInEvent(['myCmHandleId1'], ['/parent[id="forDmi1"]', '/parent[id="forDmi1"]/child'], notificationTypes, notificationFilter) >> myDmiInEvent1
101 mockDmiInEventMapper.toDmiInEvent(['myCmHandleId2'], ['/parent[id="forDmi2"]'], notificationTypes, notificationFilter) >> myDmiInEvent2
102 when: 'a subscription is created'
103 objectUnderTest.createSubscription(dataSelector, mySubId, myDataNodeSelectors)
104 then: 'each datanode selector is added using the persistence service'
105 myDataNodeSelectors.each { dataNodeSelector ->
106 1 * mockCmSubscriptionPersistenceService.add(_, dataNodeSelector)
108 and: 'an event is sent to each DMI involved'
109 1 * mockDmiInEventProducer.send(mySubId, 'myDmiService1', 'subscriptionCreateRequest', myDmiInEvent1)
110 1 * mockDmiInEventProducer.send(mySubId, 'myDmiService2', 'subscriptionCreateRequest', myDmiInEvent2)
113 def 'Process subscription CREATE request for overlapping targets [non existing & existing]'() {
114 given: 'relevant subscription details'
115 def myNewSubId = 'newId'
116 def myDataNodeSelectors = ['/newDataNodeSelector[id=""]'].toList()
117 def dataSelector = new DataSelector(notificationTypes: [], notificationFilter: '')
118 and: 'alternate id matcher always returns a cm handle id'
119 mockAlternateIdMatcher.getCmHandleId(_) >> 'someCmHandleId'
120 and: 'the inventory persistence service returns cm handles with dmi information'
121 mockInventoryPersistence.getYangModelCmHandle(_) >> new YangModelCmHandle(dmiServiceName: 'myDmiService')
122 and: 'the inventory persistence service returns inactive data node selector(s)'
123 mockCmSubscriptionPersistenceService.getInactiveDataNodeSelectors(myNewSubId) >> inactiveDataNodeSelectors
124 when: 'a subscription is created'
125 objectUnderTest.createSubscription(dataSelector, myNewSubId, myDataNodeSelectors)
126 then: 'each datanode selector is added using the persistence service'
127 1 * mockCmSubscriptionPersistenceService.add(_, myDataNodeSelectors.iterator().next())
128 and: 'an event is sent to each DMI involved'
129 expectedCallsToDmi * mockDmiInEventProducer.send(myNewSubId, 'myDmiService', 'subscriptionCreateRequest', _)
130 where: 'following data are used'
131 scenario | inactiveDataNodeSelectors || expectedCallsToDmi
132 'new target overlaps with ACCEPTED targets' | [] || 0
133 'new target overlaps with REJECTED targets' | ['/existingDataNodeSelector[id=""]', '/newDataNodeSelector[id=""]'] || 1
134 'new target overlaps with UNKNOWN targets' | ['/existingDataNodeSelector[id=""]', '/newDataNodeSelector[id=""]'] || 1
135 'new target does not overlap with existing targets' | ['/newDataNodeSelector[id=""]'] || 1
138 def 'Process subscription DELETE request where all data node selectors become unused'() {
139 given: 'a subscription id and its associated data node selectors'
140 def mySubId = 'deleteJobId'
141 def myDataNodeSelector = ['/node[id="1"]']
142 and: 'the persistence service returns the data node selectors'
143 mockCmSubscriptionPersistenceService.getDataNodeSelectors(mySubId) >> myDataNodeSelector
144 and: 'no other subscriptions exist for the data node selectors'
145 mockCmSubscriptionPersistenceService.getSubscriptionIds('/node[id="1"]') >> []
146 and: 'cm handle resolution setup'
147 def fdn = getFdn('/node[id="1"]')
148 mockAlternateIdMatcher.getCmHandleId(fdn) >> 'cmHandleId1'
149 mockInventoryPersistence.getYangModelCmHandle('cmHandleId1') >> new YangModelCmHandle(dmiServiceName: 'dmiService1')
150 and: 'DMI in event mapper returns events'
151 def deleteEvent = new DataJobSubscriptionDmiInEvent()
152 mockDmiInEventMapper.toDmiInEvent(['cmHandleId1'], ['/node[id="1"]'], null, null) >> deleteEvent
153 when: 'a subscription is deleted'
154 objectUnderTest.deleteSubscription(mySubId)
155 then: 'subscription is removed from persistence'
156 1 * mockCmSubscriptionPersistenceService.delete(mySubId, '/node[id="1"]')
157 and: 'an event is sent to each DMI involved'
158 1 * mockDmiInEventProducer.send(mySubId, 'dmiService1', 'subscriptionDeleteRequest', deleteEvent)
161 def 'Process subscription DELETE request where some data node selectors are still in use'() {
162 given: 'a subscription id and two associated selectors'
163 def mySubId = 'deleteJobId2'
164 def dataNodeSelectors = ['/node[id="1"]', '/node[id="2"]']
165 and: 'the persistence service returns the data node selectors'
166 mockCmSubscriptionPersistenceService.getDataNodeSelectors(mySubId) >> dataNodeSelectors
167 and: 'data node selector 1 has no more subscribers, data node selector 2 still has subscribers'
168 mockCmSubscriptionPersistenceService.getSubscriptionIds('/node[id="1"]') >> []
169 mockCmSubscriptionPersistenceService.getSubscriptionIds('/node[id="2"]') >> ['anotherSub']
170 and: 'cm handle resolution for data node selector 1'
171 def fdn = getFdn('/node[id="1"]')
172 mockAlternateIdMatcher.getCmHandleId(fdn) >> 'cmHandleIdX'
173 mockInventoryPersistence.getYangModelCmHandle('cmHandleIdX') >> new YangModelCmHandle(dmiServiceName: 'dmiServiceX')
174 and: 'DMI in event mapper returns events'
175 def deleteEvent = new DataJobSubscriptionDmiInEvent()
176 mockDmiInEventMapper.toDmiInEvent(['cmHandleIdX'], ['/node[id="1"]'], null, null) >> deleteEvent
177 when: 'a subscription is deleted'
178 objectUnderTest.deleteSubscription(mySubId)
179 then: 'subscription is removed from persistence for both data node selectors'
180 1 * mockCmSubscriptionPersistenceService.delete(mySubId, '/node[id="1"]')
181 1 * mockCmSubscriptionPersistenceService.delete(mySubId, '/node[id="2"]')
182 and: 'delete event is sent only for data node selectors without any subscriber'
183 1 * mockDmiInEventProducer.send(mySubId, 'dmiServiceX', 'subscriptionDeleteRequest', deleteEvent)
186 def 'Process subscription DELETE request where cmHandleId cannot be resolved'() {
187 given: 'a subscription id and its data node selector'
188 def mySubId = 'deleteJobId3'
189 def dataNodeSelectors = ['/node[id="unresolvable"]']
190 and: 'the persistence service returns the data node selectors'
191 mockCmSubscriptionPersistenceService.getDataNodeSelectors(mySubId) >> dataNodeSelectors
192 and: 'no more subscriptions exist for the data node selector'
193 mockCmSubscriptionPersistenceService.getSubscriptionIds('/node[id="unresolvable"]') >> []
194 and: 'alternate id matcher cannot resolve cm handle id'
195 def fdn = getFdn('/node[id="unresolvable"]')
196 mockAlternateIdMatcher.getCmHandleId(fdn) >> null
197 and: 'DMI in event mapper returns events'
198 def deleteEvent = new DataJobSubscriptionDmiInEvent()
199 mockDmiInEventMapper.toDmiInEvent(['cmHandleIdX'], ['/node[id="1"]'], null, null) >> deleteEvent
200 when: 'a subscription is deleted'
201 objectUnderTest.deleteSubscription(mySubId)
202 then: 'subscription is removed from persistence'
203 1 * mockCmSubscriptionPersistenceService.delete(mySubId, '/node[id="unresolvable"]')
204 and: 'no delete event is sent because cmHandleId was not resolved'
205 0 * mockDmiInEventProducer.send(*_)
208 def 'Update subscription status to ACCEPTED: #scenario'() {
209 given: 'a subscription ID'
210 def mySubscriptionId = 'mySubId'
211 and: 'the persistence service returns all inactive data node selectors'
212 def myDataNodeSelectors = ['/myDataNodeSelector[id=""]'].asList()
213 mockCmSubscriptionPersistenceService.getInactiveDataNodeSelectors(mySubscriptionId) >> myDataNodeSelectors
214 and: 'alternate id matcher always returns a cm handle id'
215 mockAlternateIdMatcher.getCmHandleId(_) >> 'someCmHandleId'
216 and: 'the inventory persistence service returns a yang model with a dmi service name for the accepted subscription'
217 mockInventoryPersistence.getYangModelCmHandle(_) >> new YangModelCmHandle(dmiServiceName: 'myDmi')
218 when: 'the method to update subscription status is called with status=ACCEPTED and dmi #dmiName'
219 objectUnderTest.updateCmSubscriptionStatus(mySubscriptionId, dmiName, ACCEPTED)
220 then: 'the persistence service to update subscription status is ONLY called for matching dmi name'
221 expectedCallsToPersistenceService * mockCmSubscriptionPersistenceService.updateCmSubscriptionStatus('/myDataNodeSelector[id=""]', ACCEPTED)
222 where: 'the following data are used'
223 scenario | dmiName || expectedCallsToPersistenceService
224 'data node selector for "myDmi"' | 'myDmi' || 1
225 'data node selector for other dmi' | 'someOtherDmi' || 0
229 def getFdn(dataNodeSelector) {
230 return JexParser.extractFdnPrefix(dataNodeSelector).orElse("")