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 static org.onap.cps.ncmp.impl.datajobs.subscription.models.CmSubscriptionStatus.ACCEPTED
24 import static org.onap.cps.ncmp.impl.datajobs.subscription.models.CmSubscriptionStatus.REJECTED
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
37 class CmSubscriptionHandlerImplSpec extends Specification {
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)
46 mockCmSubscriptionPersistenceService.isNewSubscriptionId(!'existingId') >> true
49 def objectUnderTest = new CmSubscriptionHandlerImpl(mockCmSubscriptionPersistenceService, mockDmiInEventMapper,
50 mockDmiInEventProducer, mockInventoryPersistence, mockAlternateIdMatcher)
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(*_)
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', _)
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)
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)
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
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)
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)
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(*_)
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
244 def 'Log update when subscription status is REJECTED'() {
245 given: 'dmi service name and subscription id'
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)
262 def getFdn(dataNodeSelector) {
263 return JexParser.extractFdnPrefix(dataNodeSelector).orElse("")