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
42 objectUnderTest = cpsDeltaService
43 originalCountBookstoreChildNodes = countDataNodesInBookstore()
44 originalCountBookstoreTopLevelListNodes = countTopLevelListDataNodesInBookstore()
45 originalCountXmlBookstoreChildNodes = countXmlDataNodesInBookstore()
48 def 'Get delta between 2 anchors'() {
49 when: 'attempt to get delta report between anchors'
50 def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, '/', OMIT_DESCENDANTS)
51 and: 'report is ordered based on xpath'
52 result = result.toList().sort { it.xpath }
53 then: 'delta report contains expected number of changes'
55 and: 'delta report contains REPLACE action with expected xpath'
56 assert result[0].getAction() == 'replace'
57 assert result[0].getXpath() == '/bookstore'
58 and: 'delta report contains CREATE action with expected xpath'
59 assert result[1].getAction() == 'create'
60 assert result[1].getXpath() == '/bookstore-address[@bookstore-name=\'Crossword Bookstores\']'
61 and: 'delta report contains REMOVE action with expected xpath'
62 assert result[2].getAction() == 'remove'
63 assert result[2].getXpath() == '/bookstore-address[@bookstore-name=\'Easons-1\']'
66 def 'Get delta between 2 anchors returns empty response when #scenario'() {
67 when: 'attempt to get delta report between anchors'
68 def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, targetAnchor, xpath, INCLUDE_ALL_DESCENDANTS)
69 then: 'delta report is empty'
70 assert result.isEmpty()
71 where: 'following data was used'
72 scenario | targetAnchor | xpath
73 'anchors with identical data are queried' | BOOKSTORE_ANCHOR_4 | '/'
74 'same anchor name is passed as parameter' | BOOKSTORE_ANCHOR_3 | '/'
75 'non existing xpath' | BOOKSTORE_ANCHOR_5 | '/non-existing-xpath'
78 def 'Get delta between anchors error scenario: #scenario'() {
79 when: 'attempt to get delta between anchors'
80 objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, sourceAnchor, targetAnchor, '/some-xpath', INCLUDE_ALL_DESCENDANTS)
81 then: 'expected exception is thrown'
82 thrown(expectedException)
83 where: 'following data was used'
84 scenario | dataspaceName | sourceAnchor | targetAnchor || expectedException
85 'invalid dataspace name' | 'Invalid dataspace' | 'not-relevant' | 'not-relevant' || DataValidationException
86 'invalid anchor 1 name' | FUNCTIONAL_TEST_DATASPACE_3 | 'invalid anchor' | 'not-relevant' || DataValidationException
87 'invalid anchor 2 name' | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3 | 'invalid anchor' || DataValidationException
88 'non-existing dataspace' | 'non-existing' | 'not-relevant1' | 'not-relevant2' || DataspaceNotFoundException
89 'non-existing dataspace with same anchor name' | 'non-existing' | 'not-relevant' | 'not-relevant' || DataspaceNotFoundException
90 'non-existing anchor 1' | FUNCTIONAL_TEST_DATASPACE_3 | 'non-existing-anchor' | 'not-relevant' || AnchorNotFoundException
91 'non-existing anchor 2' | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3 | 'non-existing-anchor' || AnchorNotFoundException
94 def 'Get delta between anchors for remove action, where source data node #scenario'() {
95 when: 'attempt to get delta between leaves of data nodes present in 2 anchors'
96 def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_5, BOOKSTORE_ANCHOR_3, parentNodeXpath, INCLUDE_ALL_DESCENDANTS)
97 then: 'expected action is present in delta report'
98 assert result.get(0).getAction() == 'remove'
99 where: 'following data was used'
100 scenario | parentNodeXpath
101 'has leaves and child nodes' | '/bookstore/categories[@code=\'6\']'
102 'has leaves only' | '/bookstore/categories[@code=\'5\']/books[@title=\'Book 11\']'
103 'has child data node only' | '/bookstore/support-info/contact-emails'
104 'is empty' | '/bookstore/container-without-leaves'
107 def 'Get delta between anchors for "create" action, where target data node #scenario'() {
108 when: 'attempt to get delta between leaves of data nodes present in 2 anchors'
109 def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, parentNodeXpath, INCLUDE_ALL_DESCENDANTS)
110 then: 'the expected action is present in delta report'
111 result.get(0).getAction() == 'create'
112 and: 'the expected xapth is present in delta report'
113 result.get(0).getXpath() == parentNodeXpath
114 where: 'following data was used'
115 scenario | parentNodeXpath
116 'has leaves and child nodes' | '/bookstore/categories[@code=\'6\']'
117 'has leaves only' | '/bookstore/categories[@code=\'5\']/books[@title=\'Book 11\']'
118 'has child data node only' | '/bookstore/support-info/contact-emails'
119 'is empty' | '/bookstore/container-without-leaves'
122 def 'Get delta between anchors when leaves of existing data nodes are updated,: #scenario'() {
123 when: 'attempt to get delta between leaves of existing data nodes'
124 def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, sourceAnchor, targetAnchor, xpath, OMIT_DESCENDANTS)
125 then: 'expected action is "replace"'
126 assert result[0].getAction() == 'replace'
127 and: 'the payload has expected leaf values'
128 def sourceData = result[0].getSourceData()
129 def targetData = result[0].getTargetData()
130 assert sourceData == expectedSourceValue
131 assert targetData == expectedTargetValue
132 where: 'following data was used'
133 scenario | sourceAnchor | targetAnchor | xpath || expectedSourceValue | expectedTargetValue
134 'leaf is updated in target anchor' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore' || ['bookstore-name': 'Easons-1'] | ['bookstore-name': 'Crossword Bookstores']
135 'leaf is removed in target anchor' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore/categories[@code=\'5\']/books[@title=\'Book 1\']' || [price:1] | null
136 'leaf is added in target anchor' | BOOKSTORE_ANCHOR_5 | BOOKSTORE_ANCHOR_3 | '/bookstore/categories[@code=\'5\']/books[@title=\'Book 1\']' || null | [price:1]
139 def 'Get delta between anchors when child data nodes under existing parent data nodes are updated: #scenario'() {
140 when: 'attempt to get delta between leaves of existing data nodes'
141 def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, sourceAnchor, targetAnchor, xpath, DIRECT_CHILDREN_ONLY)
142 then: 'expected action is "replace"'
143 assert result[0].getAction() == 'replace'
144 and: 'the delta report has expected child node xpaths'
145 def deltaReportEntities = getDeltaReportEntities(result)
146 def childNodeXpathsInDeltaReport = deltaReportEntities.get('xpaths')
147 assert childNodeXpathsInDeltaReport.contains(expectedChildNodeXpath)
148 where: 'following data was used'
149 scenario | sourceAnchor | targetAnchor | xpath || expectedChildNodeXpath
150 '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\']'
151 'removed child data nodes in target anchor' | BOOKSTORE_ANCHOR_5 | BOOKSTORE_ANCHOR_3 | '/bookstore' || '/bookstore/support-info'
152 'added child data nodes in target anchor' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore' || '/bookstore/support-info'
155 def 'Get delta between anchors where source and target data nodes have leaves and child data nodes'() {
156 given: 'parent node xpath and expected data in delta report'
157 def parentNodeXpath = '/bookstore/categories[@code=\'1\']'
158 def expectedSourceDataInParentNode = ['name':'Children']
159 def expectedTargetDataInParentNode = ['name':'Kids']
160 def expectedSourceDataInChildNode = [['lang' : 'English'],['price':20, 'editions':[1988, 2000]]]
161 def expectedTargetDataInChildNode = [['lang':'English/German'], ['price':200, 'editions':[1988, 2000, 2023]]]
162 when: 'attempt to get delta between leaves of existing data nodes'
163 def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, parentNodeXpath, INCLUDE_ALL_DESCENDANTS)
164 def deltaReportEntities = getDeltaReportEntities(result)
165 then: 'expected action is "replace"'
166 assert result[0].getAction() == 'replace'
167 and: 'the payload has expected parent node xpath'
168 assert deltaReportEntities.get('xpaths').contains(parentNodeXpath)
169 and: 'delta report has expected source and target data'
170 assert deltaReportEntities.get('sourcePayload').contains(expectedSourceDataInParentNode)
171 assert deltaReportEntities.get('targetPayload').contains(expectedTargetDataInParentNode)
172 and: 'the delta report also has expected child node xpaths'
173 assert deltaReportEntities.get('xpaths').containsAll(['/bookstore/categories[@code=\'1\']/books[@title=\'The Gruffalo\']', '/bookstore/categories[@code=\'1\']/books[@title=\'Matilda\']'])
174 and: 'the delta report also has expected source and target data of child nodes'
175 assert deltaReportEntities.get('sourcePayload').containsAll(expectedSourceDataInChildNode)
176 assert deltaReportEntities.get('targetPayload').containsAll(expectedTargetDataInChildNode)
179 def 'Get delta between anchor and JSON payload'() {
180 when: 'attempt to get delta report between anchor and JSON payload'
181 def jsonPayload = '{\"book-store:bookstore\":{\"bookstore-name\":\"Crossword Bookstores\"},\"book-store:bookstore-address\":{\"address\":\"Bangalore, India\",\"postal-code\":\"560062\",\"bookstore-name\":\"Crossword Bookstores\"}}'
182 def result = objectUnderTest.getDeltaByDataspaceAnchorAndPayload(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, '/', [:], jsonPayload, OMIT_DESCENDANTS)
183 then: 'delta report contains expected number of changes'
185 and: 'delta report contains "replace" action with expected xpath'
186 assert result[0].getAction() == 'replace'
187 assert result[0].getXpath() == '/bookstore'
188 and: 'delta report contains "remove" action with expected xpath'
189 assert result[1].getAction() == 'remove'
190 assert result[1].getXpath() == '/bookstore-address[@bookstore-name=\'Easons-1\']'
191 and: 'delta report contains "create" action with expected xpath'
192 assert result[2].getAction() == 'create'
193 assert result[2].getXpath() == '/bookstore-address[@bookstore-name=\'Crossword Bookstores\']'
196 def 'Get delta between anchor and payload returns empty response when JSON payload is identical to anchor data'() {
197 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)'
198 def jsonPayload = readResourceDataFile('bookstore/bookstoreData.json').replace('Easons', 'Easons-1')
199 def result = objectUnderTest.getDeltaByDataspaceAnchorAndPayload(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, '/', [:], jsonPayload, INCLUDE_ALL_DESCENDANTS)
200 then: 'delta report is empty'
201 assert result.isEmpty()
204 def 'Get delta between anchor and payload error scenario: #scenario'() {
205 when: 'attempt to get delta between anchor and json payload'
206 objectUnderTest.getDeltaByDataspaceAnchorAndPayload(dataspaceName, sourceAnchor, xpath, [:], jsonPayload, INCLUDE_ALL_DESCENDANTS)
207 then: 'expected exception is thrown'
208 thrown(expectedException)
209 where: 'following data was used'
210 scenario | dataspaceName | sourceAnchor | xpath | jsonPayload || expectedException
211 'invalid dataspace name' | 'Invalid dataspace' | 'not-relevant' | '/' | '{some-json}' || DataValidationException
212 'invalid anchor name' | FUNCTIONAL_TEST_DATASPACE_3 | 'invalid anchor' | '/' | '{some-json}' || DataValidationException
213 'non-existing dataspace' | 'non-existing' | 'not-relevant' | '/' | '{some-json}' || DataspaceNotFoundException
214 'non-existing anchor' | FUNCTIONAL_TEST_DATASPACE_3 | 'non-existing-anchor' | '/' | '{some-json}' || AnchorNotFoundException
215 'empty json payload with root node xpath' | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3 | '/' | '' || DataValidationException
216 'empty json payload with non-root node xpath' | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3 | '/bookstore' | '' || DataValidationException
219 def getDeltaReportEntities(List<DeltaReport> deltaReport) {
222 def sourcePayload = []
223 def targetPayload = []
225 delta -> xpaths.add(delta.getXpath())
226 action.add(delta.getAction())
227 sourcePayload.add(delta.getSourceData())
228 targetPayload.add(delta.getTargetData())
230 return ['xpaths':xpaths, 'action':action, 'sourcePayload':sourcePayload, 'targetPayload':targetPayload]
233 def countDataNodesInBookstore() {
234 return countDataNodesInTree(cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', INCLUDE_ALL_DESCENDANTS))
237 def countTopLevelListDataNodesInBookstore() {
238 return countDataNodesInTree(cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/', INCLUDE_ALL_DESCENDANTS))
241 def countXmlDataNodesInBookstore() {
242 return countDataNodesInTree(cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_4, BOOKSTORE_ANCHOR_6, '/bookstore', INCLUDE_ALL_DESCENDANTS))