adc8803eb8dd424a091db5752259712b768a7cce
[cps.git] /
1 /*
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
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.datajobs.subscription.utils
23
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
31
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
44
45 class CmSubscriptionPersistenceServiceSpec extends Specification {
46
47     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
48     def mockCpsQueryService = Mock(CpsQueryService)
49     def mockCpsDataService = Mock(CpsDataService)
50     def logAppender = Spy(ListAppender<ILoggingEvent>)
51
52     def objectUnderTest = new CmDataJobSubscriptionPersistenceService(jsonObjectMapper, mockCpsQueryService, mockCpsDataService)
53
54     void setup() {
55         def logger = LoggerFactory.getLogger(CmDataJobSubscriptionPersistenceService)
56         logger.addAppender(logAppender)
57         logAppender.start()
58     }
59
60     void cleanup() {
61         ((Logger) LoggerFactory.getLogger(CmDataJobSubscriptionPersistenceService.class)).detachAndStopAllAppenders()
62     }
63
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
77     }
78
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
92     }
93
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
106     }
107
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)
121     }
122
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)
136     }
137
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' )
152     }
153
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, _)
169     }
170
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)
186     }
187
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'
196             noExceptionThrown()
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'
202     }
203
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, _, _)
217     }
218 }