2 * ============LICENSE_START=======================================================
3 * Copyright (c) 2024-2025 OpenInfra Foundation Europe. All rights reserved.
4 * Modifications Copyright (C) 2024 TechMahindra Ltd.
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.impl.datajobs.subscription.utils
24 import static CmDataJobSubscriptionPersistenceService.CPS_PATH_TEMPLATE_FOR_SUBSCRIPTIONS_WITH_DATA_NODE_SELECTOR
25 import static CmDataJobSubscriptionPersistenceService.CPS_PATH_TEMPLATE_FOR_SUBSCRIPTION_WITH_DATA_JOB_ID
26 import static CmDataJobSubscriptionPersistenceService.CPS_PATH_TEMPLATE_FOR_INACTIVE_SUBSCRIPTIONS
27 import static CmDataJobSubscriptionPersistenceService.CPS_PATH_FOR_SUBSCRIPTION_WITH_DATA_NODE_SELECTOR
28 import static CmDataJobSubscriptionPersistenceService.PARENT_NODE_XPATH
29 import static org.onap.cps.ncmp.impl.datajobs.subscription.models.CmSubscriptionStatus.ACCEPTED
30 import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS
32 import ch.qos.logback.classic.Level
33 import ch.qos.logback.classic.Logger
34 import ch.qos.logback.classic.spi.ILoggingEvent
35 import ch.qos.logback.core.read.ListAppender
36 import org.slf4j.LoggerFactory
37 import com.fasterxml.jackson.databind.ObjectMapper
38 import org.onap.cps.api.CpsDataService
39 import org.onap.cps.api.CpsQueryService
40 import org.onap.cps.api.model.DataNode
41 import org.onap.cps.utils.ContentType
42 import org.onap.cps.utils.JsonObjectMapper
43 import spock.lang.Specification
45 class CmSubscriptionPersistenceServiceSpec extends Specification {
47 def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
48 def mockCpsQueryService = Mock(CpsQueryService)
49 def mockCpsDataService = Mock(CpsDataService)
50 def logAppender = Spy(ListAppender<ILoggingEvent>)
52 def objectUnderTest = new CmDataJobSubscriptionPersistenceService(jsonObjectMapper, mockCpsQueryService, mockCpsDataService)
55 def logger = LoggerFactory.getLogger(CmDataJobSubscriptionPersistenceService)
56 logger.addAppender(logAppender)
61 ((Logger) LoggerFactory.getLogger(CmDataJobSubscriptionPersistenceService.class)).detachAndStopAllAppenders()
64 def 'Check cm data job subscription details has at least one subscriber #scenario'() {
65 given: 'a valid cm data job subscription query'
66 def cpsPathQuery = CPS_PATH_TEMPLATE_FOR_SUBSCRIPTIONS_WITH_DATA_NODE_SELECTOR.formatted('/myDataNodeSelector')
67 and: 'datanodes optionally returned'
68 1 * mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-job-subscriptions', cpsPathQuery, OMIT_DESCENDANTS) >> dataNode
69 when: 'we check if subscription details already has at least one subscriber'
70 def result = objectUnderTest.hasAtLeastOneSubscription('/myDataNodeSelector')
71 then: 'we get expected result'
72 assert result == hasAtLeastOneSubscription
73 where: 'following scenarios are used'
74 scenario | dataNode || hasAtLeastOneSubscription
75 'valid datanodes present' | [new DataNode(leaves: ['dataJobId': ['dataJobId1']])] || true
76 'no datanodes present' | [] || false
79 def 'Checking uniqueness of incoming subscription ID'() {
80 given: 'a cps path with a data job subscription ID for querying'
81 def cpsPathQuery = CPS_PATH_TEMPLATE_FOR_SUBSCRIPTION_WITH_DATA_JOB_ID.formatted('mySubId')
82 and: 'collection of data nodes are returned'
83 1 * mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-job-subscriptions', cpsPathQuery, OMIT_DESCENDANTS) >> dataNodes
84 when: 'a data job subscription id is tested for uniqueness'
85 def result = objectUnderTest.isNewSubscriptionId('mySubId')
86 then: 'result is as expected'
87 assert result == isValidDataJobSubscriptionId
88 where: 'following scenarios are used'
89 scenario | dataNodes || isValidDataJobSubscriptionId
90 'datanodes present' | [new DataNode()] || false
91 'no datanodes present' | [] || true
94 def 'Get all inactive data node selectors for subscription id'() {
95 given: 'the query service returns nodes for subscription id'
96 def expectedDataNode = new DataNode(leaves: ['datajobId': ['id1'], 'dataNodeSelector': '/dataNodeSelector', 'status': 'UNKNOWN'])
97 def queryServiceResponse = [expectedDataNode].asCollection()
98 def cmDataJobSubscriptionIdCpsPath = CPS_PATH_TEMPLATE_FOR_INACTIVE_SUBSCRIPTIONS.formatted('id1')
99 1 * mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-job-subscriptions', cmDataJobSubscriptionIdCpsPath, OMIT_DESCENDANTS) >> queryServiceResponse
100 when: 'retrieving all nodes for data job subscription id'
101 def result = objectUnderTest.getInactiveDataNodeSelectors('id1')
102 then: 'the result returns correct number of datanodes'
103 assert result.size() == 1
104 and: 'the attribute of the data nodes is as expected'
105 assert result.iterator().next() == expectedDataNode.leaves.dataNodeSelector
108 def 'Add subscription for a data node selector that have no subscriptions yet.'() {
109 given: 'a valid cm data job subscription path query'
110 def dataNodeSelector = '/myDataNodeSelector'
111 def query = CPS_PATH_TEMPLATE_FOR_SUBSCRIPTIONS_WITH_DATA_NODE_SELECTOR.formatted(dataNodeSelector)
112 and: 'a data node does not exist for cm data job subscription path query'
113 mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-job-subscriptions', query, OMIT_DESCENDANTS) >> []
114 and: 'data job subscription details is mapped as JSON'
115 def subscriptionIds = ['newSubId']
116 def subscriptionAsJson = objectUnderTest.createSubscriptionDetailsAsJson(dataNodeSelector, subscriptionIds, 'UNKNOWN')
117 when: 'the method to add cm notification subscription is called'
118 objectUnderTest.add('newSubId', dataNodeSelector)
119 then: 'data service method to create new subscription for given subscriber is called once with the correct parameters'
120 1 * mockCpsDataService.saveData('NCMP-Admin', 'cm-data-job-subscriptions', PARENT_NODE_XPATH, subscriptionAsJson, _, ContentType.JSON)
123 def 'Add subscription for a data node selector that already have subscription(s).'() {
124 given: 'a valid cm subscription path query'
125 def dataNodeSelector = '/myDataNodeSelector'
126 def query = CPS_PATH_TEMPLATE_FOR_SUBSCRIPTIONS_WITH_DATA_NODE_SELECTOR.formatted(dataNodeSelector)
127 and: 'a dataNode exists for the given cps path query'
128 mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-job-subscriptions', query, OMIT_DESCENDANTS) >> [new DataNode(leaves: ['dataJobId': ['existingId'], 'dataNodeSelector': dataNodeSelector, 'status': 'ACCEPTED'])]
129 and: 'updated cm data job subscription details as json'
130 def newListOfSubscriptionIds = ['existingId', 'newSubId']
131 def subscriptionDetailsAsJson = objectUnderTest.createSubscriptionDetailsAsJson(dataNodeSelector, newListOfSubscriptionIds, 'ACCEPTED')
132 when: 'the method to add cm notification subscription is called'
133 objectUnderTest.add('newSubId', dataNodeSelector)
134 then: 'data service method to update list of subscribers is called once'
135 1 * mockCpsDataService.updateNodeLeaves('NCMP-Admin', 'cm-data-job-subscriptions', PARENT_NODE_XPATH, subscriptionDetailsAsJson, _, ContentType.JSON)
138 def 'Get data node selectors by subscription id.'() {
139 given: 'a subscription id and a corresponding CPS query path'
140 def subscriptionId = 'mySubId'
141 def cpsPathQuery = CPS_PATH_TEMPLATE_FOR_SUBSCRIPTION_WITH_DATA_JOB_ID.formatted(subscriptionId)
142 and: 'the query service returns a collection of DataNodes with dataNodeSelectors'
143 def expectedDataNode1 = new DataNode(leaves: ['dataNodeSelector': '/dataNodeSelector1'])
144 def expectedDataNode2 = new DataNode(leaves: ['dataNodeSelector': '/dataNodeSelector2'])
145 def queryServiceResponse = [expectedDataNode1, expectedDataNode2]
146 1 * mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-job-subscriptions', cpsPathQuery, OMIT_DESCENDANTS) >> queryServiceResponse
147 when: 'get data node selectors by subscription id is called'
148 def result = objectUnderTest.getDataNodeSelectors(subscriptionId)
149 then: 'the returned list contains the correct data node selectors'
150 assert result.size() == 2
151 assert result.containsAll('/dataNodeSelector1', '/dataNodeSelector2' )
154 def 'Delete subscription removes last subscriber.'() {
155 given: 'a dataNode with only one subscription'
156 def dataNodeSelector = '/myDataNodeSelector'
157 def subscriptionId = 'someId'
158 def queryForDataNode = CPS_PATH_TEMPLATE_FOR_SUBSCRIPTIONS_WITH_DATA_NODE_SELECTOR.formatted(dataNodeSelector)
159 def queryForDelete = CPS_PATH_FOR_SUBSCRIPTION_WITH_DATA_NODE_SELECTOR.formatted(dataNodeSelector)
160 def dataNode = new DataNode(leaves: ['dataJobId': [subscriptionId], 'status': 'ACCEPTED'])
161 mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-job-subscriptions', queryForDataNode, OMIT_DESCENDANTS) >> [dataNode]
162 and: 'subscription IDs for the data node'
163 objectUnderTest = Spy(objectUnderTest)
164 objectUnderTest.getSubscriptionIds(dataNodeSelector) >> [subscriptionId].toList()
165 when: 'delete method is called'
166 objectUnderTest.delete(subscriptionId, dataNodeSelector)
167 then: 'subscription deletion is performed'
168 1 * mockCpsDataService.deleteDataNode('NCMP-Admin', 'cm-data-job-subscriptions', queryForDelete, _)
171 def 'Delete subscription removes one of multiple subscribers.'() {
172 given: 'a dataNode with multiple subscriptions'
173 def dataNodeSelector = '/myDataNodeSelector'
174 def query = CPS_PATH_TEMPLATE_FOR_SUBSCRIPTIONS_WITH_DATA_NODE_SELECTOR.formatted(dataNodeSelector)
175 def dataNode = new DataNode(leaves: ['dataJobId': ['id-to-remove', 'id-remaining'], 'status': 'ACCEPTED'])
176 mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-job-subscriptions', query, OMIT_DESCENDANTS) >> [dataNode]
177 and: 'subscription IDs for the data node'
178 objectUnderTest.getSubscriptionIds(dataNodeSelector) >> ['id-to-remove', 'id-remaining']
179 when: 'delete method is called'
180 objectUnderTest.delete('id-to-remove', dataNodeSelector)
181 then: 'data service is called to update leaves with remaining subscription'
182 1 * mockCpsDataService.updateNodeLeaves('NCMP-Admin', 'cm-data-job-subscriptions', PARENT_NODE_XPATH, { json ->
183 json.contains('"status":"ACCEPTED"') &&
184 json.contains('"dataJobId":["id-remaining"]')
185 }, _, ContentType.JSON)
188 def 'Delete subscription that does not exist'() {
189 given: 'the query service returns data node for given data node selector'
190 def query = CPS_PATH_TEMPLATE_FOR_SUBSCRIPTIONS_WITH_DATA_NODE_SELECTOR.formatted('/myDataNodeSelector')
191 def dataNode = new DataNode(leaves: ['dataJobId': ['some-id'], 'status': 'ACCEPTED'])
192 mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-job-subscriptions', query, OMIT_DESCENDANTS) >> [dataNode]
193 when: 'deleting a subscription on a data node selector'
194 objectUnderTest.delete('non-existing-id', '/myDataNodeSelector')
195 then: 'no exception thrown'
197 and: 'an event is logged with level INFO'
198 def loggingEvent = logAppender.list[0]
199 assert loggingEvent.level == Level.WARN
200 and: 'the log indicates subscription id does not exist for data node selector'
201 assert loggingEvent.formattedMessage == 'SubscriptionId=non-existing-id not found under dataNodeSelector=/myDataNodeSelector'
204 def 'Update status of a subscription.'() {
205 given: 'a data node selector and status'
206 def myDataNodeSelector = "/myDataNodeSelector"
207 def status = ACCEPTED
208 and: 'the query service returns data node'
209 def subscriptionIds = ['someId']
210 mockCpsQueryService.queryDataNodes(*_) >> [new DataNode(leaves: ['dataJobId': subscriptionIds, 'dataNodeSelector': myDataNodeSelector, 'status': 'UNKNOWN'])]
211 and: 'updated cm data job subscription details as json'
212 def subscriptionDetailsAsJson = objectUnderTest.createSubscriptionDetailsAsJson(myDataNodeSelector, subscriptionIds, status.name())
213 when: 'the method to update subscription status is called'
214 objectUnderTest.updateCmSubscriptionStatus(myDataNodeSelector, status)
215 then: 'data service method to update list of subscribers is called once'
216 1 * mockCpsDataService.updateNodeLeaves('NCMP-Admin', 'cm-data-job-subscriptions', PARENT_NODE_XPATH, subscriptionDetailsAsJson, _, _)