2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2025 TechMahindra Ltd.
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.integration.functional.cps
23 import org.onap.cps.api.CpsDeltaService
24 import org.onap.cps.api.exceptions.AnchorNotFoundException
25 import org.onap.cps.api.exceptions.DataValidationException
26 import org.onap.cps.api.exceptions.DataspaceNotFoundException
27 import org.onap.cps.api.model.DeltaReport
28 import org.onap.cps.api.parameters.FetchDescendantsOption
29 import org.onap.cps.integration.base.FunctionalSpecBase
31 class DeltaServiceIntegrationSpec extends FunctionalSpecBase {
32 CpsDeltaService objectUnderTest
33 def originalCountBookstoreChildNodes
34 def originalCountXmlBookstoreChildNodes
35 def originalCountBookstoreTopLevelListNodes
37 static def INCLUDE_ALL_DESCENDANTS = FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
38 static def OMIT_DESCENDANTS = FetchDescendantsOption.OMIT_DESCENDANTS
39 static def DIRECT_CHILDREN_ONLY = FetchDescendantsOption.DIRECT_CHILDREN_ONLY
40 def NO_GROUPING = false
43 objectUnderTest = cpsDeltaService
44 originalCountBookstoreChildNodes = countDataNodesInBookstore()
45 originalCountBookstoreTopLevelListNodes = countTopLevelListDataNodesInBookstore()
46 originalCountXmlBookstoreChildNodes = countXmlDataNodesInBookstore()
49 def 'Get delta between 2 anchors'() {
50 when: 'attempt to get delta report between anchors'
51 def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, '/', OMIT_DESCENDANTS, NO_GROUPING)
52 and: 'report is ordered based on xpath'
53 result = result.toList().sort { it.xpath }
54 then: 'delta report contains expected number of changes'
56 and: 'delta report contains REPLACE action with expected xpath'
57 assert result[0].getAction() == 'replace'
58 assert result[0].getXpath() == '/bookstore'
59 and: 'delta report contains CREATE action with expected xpath'
60 assert result[1].getAction() == 'create'
61 assert result[1].getXpath() == '/bookstore-address[@bookstore-name=\'Crossword Bookstores\']'
62 and: 'delta report contains REMOVE action with expected xpath'
63 assert result[2].getAction() == 'remove'
64 assert result[2].getXpath() == '/bookstore-address[@bookstore-name=\'Easons-1\']'
67 def 'Get delta between 2 anchors returns empty response when #scenario'() {
68 when: 'attempt to get delta report between anchors'
69 def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, targetAnchor, xpath, INCLUDE_ALL_DESCENDANTS, NO_GROUPING)
70 then: 'delta report is empty'
71 assert result.isEmpty()
72 where: 'following data was used'
73 scenario | targetAnchor | xpath
74 'anchors with identical data are queried' | BOOKSTORE_ANCHOR_4 | '/'
75 'same anchor name is passed as parameter' | BOOKSTORE_ANCHOR_3 | '/'
76 'non existing xpath' | BOOKSTORE_ANCHOR_5 | '/non-existing-xpath'
79 def 'Get delta between anchors error scenario: #scenario'() {
80 when: 'attempt to get delta between anchors'
81 objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, sourceAnchor, targetAnchor, '/some-xpath', INCLUDE_ALL_DESCENDANTS, NO_GROUPING)
82 then: 'expected exception is thrown'
83 thrown(expectedException)
84 where: 'following data was used'
85 scenario | dataspaceName | sourceAnchor | targetAnchor || expectedException
86 'invalid dataspace name' | 'Invalid dataspace' | 'not-relevant' | 'not-relevant' || DataValidationException
87 'invalid anchor 1 name' | FUNCTIONAL_TEST_DATASPACE_3 | 'invalid anchor' | 'not-relevant' || DataValidationException
88 'invalid anchor 2 name' | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3 | 'invalid anchor' || DataValidationException
89 'non-existing dataspace' | 'non-existing' | 'not-relevant1' | 'not-relevant2' || DataspaceNotFoundException
90 'non-existing dataspace with same anchor name' | 'non-existing' | 'not-relevant' | 'not-relevant' || DataspaceNotFoundException
91 'non-existing anchor 1' | FUNCTIONAL_TEST_DATASPACE_3 | 'non-existing-anchor' | 'not-relevant' || AnchorNotFoundException
92 'non-existing anchor 2' | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3 | 'non-existing-anchor' || AnchorNotFoundException
95 def 'Get delta between anchors for remove action, where source data node #scenario'() {
96 when: 'attempt to get delta between leaves of data nodes present in 2 anchors'
97 def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_5, BOOKSTORE_ANCHOR_3, parentNodeXpath, INCLUDE_ALL_DESCENDANTS, NO_GROUPING)
98 then: 'expected action is present in delta report'
99 assert result.get(0).getAction() == 'remove'
100 where: 'following data was used'
101 scenario | parentNodeXpath
102 'has leaves and child nodes' | '/bookstore/categories[@code=\'6\']'
103 'has leaves only' | '/bookstore/categories[@code=\'5\']/books[@title=\'Book 11\']'
104 'has child data node only' | '/bookstore/support-info/contact-emails'
105 'is empty' | '/bookstore/container-without-leaves'
108 def 'Get delta between anchors for "create" action, where target data node #scenario'() {
109 when: 'attempt to get delta between leaves of data nodes present in 2 anchors'
110 def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, parentNodeXpath, INCLUDE_ALL_DESCENDANTS, NO_GROUPING)
111 then: 'the expected action is present in delta report'
112 result.get(0).getAction() == 'create'
113 and: 'the expected xapth is present in delta report'
114 result.get(0).getXpath() == parentNodeXpath
115 where: 'following data was used'
116 scenario | parentNodeXpath
117 'has leaves and child nodes' | '/bookstore/categories[@code=\'6\']'
118 'has leaves only' | '/bookstore/categories[@code=\'5\']/books[@title=\'Book 11\']'
119 'has child data node only' | '/bookstore/support-info/contact-emails'
120 'is empty' | '/bookstore/container-without-leaves'
123 def 'Get delta between anchors when leaves of existing data nodes are updated,: #scenario'() {
124 when: 'attempt to get delta between leaves of existing data nodes'
125 def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, sourceAnchor, targetAnchor, xpath, OMIT_DESCENDANTS, NO_GROUPING)
126 then: 'expected action is "replace"'
127 assert result[0].getAction() == 'replace'
128 and: 'the payload has expected leaf values'
129 def sourceData = result[0].getSourceData()
130 def targetData = result[0].getTargetData()
131 assert sourceData == expectedSourceValue
132 assert targetData == expectedTargetValue
133 where: 'following data was used'
134 scenario | sourceAnchor | targetAnchor | xpath || expectedSourceValue | expectedTargetValue
135 'leaf is updated in target anchor' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore' || ['bookstore-name': 'Easons-1'] | ['bookstore-name': 'Crossword Bookstores']
136 'leaf is removed in target anchor' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore/categories[@code=\'5\']/books[@title=\'Book 1\']' || [price:1] | null
137 'leaf is added in target anchor' | BOOKSTORE_ANCHOR_5 | BOOKSTORE_ANCHOR_3 | '/bookstore/categories[@code=\'5\']/books[@title=\'Book 1\']' || null | [price:1]
140 def 'Get delta between anchors when child data nodes under existing parent data nodes are updated: #scenario'() {
141 when: 'attempt to get delta between leaves of existing data nodes'
142 def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, sourceAnchor, targetAnchor, xpath, DIRECT_CHILDREN_ONLY, NO_GROUPING)
143 then: 'expected action is "replace"'
144 assert result[0].getAction() == 'replace'
145 and: 'the delta report has expected child node xpaths'
146 def deltaReportEntities = getDeltaReportEntities(result)
147 def childNodeXpathsInDeltaReport = deltaReportEntities.get('xpaths')
148 assert childNodeXpathsInDeltaReport.contains(expectedChildNodeXpath)
149 where: 'following data was used'
150 scenario | sourceAnchor | targetAnchor | xpath || expectedChildNodeXpath
151 'source and target anchors have child data nodes' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore/premises' || '/bookstore/premises/addresses[@house-number=\'2\' and @street=\'Main Street\']'
152 'removed child data nodes in target anchor' | BOOKSTORE_ANCHOR_5 | BOOKSTORE_ANCHOR_3 | '/bookstore' || '/bookstore/support-info'
153 'added child data nodes in target anchor' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore' || '/bookstore/support-info'
156 def 'Get delta between anchors where source and target data nodes have leaves and child data nodes'() {
157 given: 'parent node xpath and expected data in delta report'
158 def parentNodeXpath = '/bookstore/categories[@code=\'1\']'
159 def expectedSourceDataInParentNode = ['name':'Children']
160 def expectedTargetDataInParentNode = ['name':'Kids']
161 def expectedSourceDataInChildNode = [['lang' : 'English'],['price':20, 'editions':[1988, 2000]]]
162 def expectedTargetDataInChildNode = [['lang':'English/German'], ['price':200, 'editions':[1988, 2000, 2023]]]
163 when: 'attempt to get delta between leaves of existing data nodes'
164 def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, parentNodeXpath, INCLUDE_ALL_DESCENDANTS, NO_GROUPING)
165 def deltaReportEntities = getDeltaReportEntities(result)
166 then: 'expected action is "replace"'
167 assert result[0].getAction() == 'replace'
168 and: 'the payload has expected parent node xpath'
169 assert deltaReportEntities.get('xpaths').contains(parentNodeXpath)
170 and: 'delta report has expected source and target data'
171 assert deltaReportEntities.get('sourcePayload').contains(expectedSourceDataInParentNode)
172 assert deltaReportEntities.get('targetPayload').contains(expectedTargetDataInParentNode)
173 and: 'the delta report also has expected child node xpaths'
174 assert deltaReportEntities.get('xpaths').containsAll(['/bookstore/categories[@code=\'1\']/books[@title=\'The Gruffalo\']', '/bookstore/categories[@code=\'1\']/books[@title=\'Matilda\']'])
175 and: 'the delta report also has expected source and target data of child nodes'
176 assert deltaReportEntities.get('sourcePayload').containsAll(expectedSourceDataInChildNode)
177 assert deltaReportEntities.get('targetPayload').containsAll(expectedTargetDataInChildNode)
180 def 'Get delta between anchor and JSON payload'() {
181 when: 'attempt to get delta report between anchor and JSON payload'
182 def jsonPayload = '{\"book-store:bookstore\":{\"bookstore-name\":\"Crossword Bookstores\"},\"book-store:bookstore-address\":{\"address\":\"Bangalore, India\",\"postal-code\":\"560062\",\"bookstore-name\":\"Crossword Bookstores\"}}'
183 def result = objectUnderTest.getDeltaByDataspaceAnchorAndPayload(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, '/', [:], jsonPayload, OMIT_DESCENDANTS, NO_GROUPING)
184 then: 'delta report contains expected number of changes'
186 and: 'delta report contains "replace" action with expected xpath'
187 assert result[0].getAction() == 'replace'
188 assert result[0].getXpath() == '/bookstore'
189 and: 'delta report contains "remove" action with expected xpath'
190 assert result[1].getAction() == 'remove'
191 assert result[1].getXpath() == '/bookstore-address[@bookstore-name=\'Easons-1\']'
192 and: 'delta report contains "create" action with expected xpath'
193 assert result[2].getAction() == 'create'
194 assert result[2].getXpath() == '/bookstore-address[@bookstore-name=\'Crossword Bookstores\']'
197 def 'Get delta between anchor and payload returns empty response when JSON payload is identical to anchor data'() {
198 when: 'attempt to get delta report between anchor and JSON payload (replacing the string Easons with Easons-1 because the data in JSON file is modified, to append anchor number, during the setup process of the integration tests)'
199 def jsonPayload = readResourceDataFile('bookstore/bookstoreData.json').replace('Easons', 'Easons-1')
200 def result = objectUnderTest.getDeltaByDataspaceAnchorAndPayload(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, '/', [:], jsonPayload, INCLUDE_ALL_DESCENDANTS, NO_GROUPING)
201 then: 'delta report is empty'
202 assert result.isEmpty()
205 def 'Get delta between anchor and payload error scenario: #scenario'() {
206 when: 'attempt to get delta between anchor and json payload'
207 objectUnderTest.getDeltaByDataspaceAnchorAndPayload(dataspaceName, sourceAnchor, xpath, [:], jsonPayload, INCLUDE_ALL_DESCENDANTS, NO_GROUPING)
208 then: 'expected exception is thrown'
209 thrown(expectedException)
210 where: 'following data was used'
211 scenario | dataspaceName | sourceAnchor | xpath | jsonPayload || expectedException
212 'invalid dataspace name' | 'Invalid dataspace' | 'not-relevant' | '/' | '{some-json}' || DataValidationException
213 'invalid anchor name' | FUNCTIONAL_TEST_DATASPACE_3 | 'invalid anchor' | '/' | '{some-json}' || DataValidationException
214 'non-existing dataspace' | 'non-existing' | 'not-relevant' | '/' | '{some-json}' || DataspaceNotFoundException
215 'non-existing anchor' | FUNCTIONAL_TEST_DATASPACE_3 | 'non-existing-anchor' | '/' | '{some-json}' || AnchorNotFoundException
216 'empty json payload with root node xpath' | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3 | '/' | '' || DataValidationException
217 'empty json payload with non-root node xpath' | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3 | '/bookstore' | '' || DataValidationException
220 def getDeltaReportEntities(List<DeltaReport> deltaReport) {
223 def sourcePayload = []
224 def targetPayload = []
226 delta -> xpaths.add(delta.getXpath())
227 action.add(delta.getAction())
228 sourcePayload.add(delta.getSourceData())
229 targetPayload.add(delta.getTargetData())
231 return ['xpaths':xpaths, 'action':action, 'sourcePayload':sourcePayload, 'targetPayload':targetPayload]
234 def countDataNodesInBookstore() {
235 return countDataNodesInTree(cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', INCLUDE_ALL_DESCENDANTS))
238 def countTopLevelListDataNodesInBookstore() {
239 return countDataNodesInTree(cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/', INCLUDE_ALL_DESCENDANTS))
242 def countXmlDataNodesInBookstore() {
243 return countDataNodesInTree(cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_4, BOOKSTORE_ANCHOR_6, '/bookstore', INCLUDE_ALL_DESCENDANTS))