691e71427c548a8bb85d41260f11cb5e2c9263f1
[cps.git] /
1 /*
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
8  *
9  *        http://www.apache.org/licenses/LICENSE-2.0
10  *
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.
16  *
17  *  SPDX-License-Identifier: Apache-2.0
18  *  ============LICENSE_END=========================================================
19  */
20
21 package org.onap.cps.integration.functional.cps
22
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
30
31 class DeltaServiceIntegrationSpec extends FunctionalSpecBase {
32     CpsDeltaService objectUnderTest
33     def originalCountBookstoreChildNodes
34     def originalCountXmlBookstoreChildNodes
35     def originalCountBookstoreTopLevelListNodes
36
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
41     def setup() {
42         objectUnderTest = cpsDeltaService
43         originalCountBookstoreChildNodes = countDataNodesInBookstore()
44         originalCountBookstoreTopLevelListNodes = countTopLevelListDataNodesInBookstore()
45         originalCountXmlBookstoreChildNodes = countXmlDataNodesInBookstore()
46     }
47
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'
54             result.size() == 3
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\']'
64     }
65
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'
76     }
77
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
92     }
93
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'
105     }
106
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'
120     }
121
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]
137     }
138
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'
153     }
154
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)
177     }
178
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'
184             result.size() == 3
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\']'
194     }
195
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()
202     }
203
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
217     }
218
219     def getDeltaReportEntities(List<DeltaReport> deltaReport) {
220         def xpaths = []
221         def action = []
222         def sourcePayload = []
223         def targetPayload = []
224         deltaReport.each {
225             delta -> xpaths.add(delta.getXpath())
226                 action.add(delta.getAction())
227                 sourcePayload.add(delta.getSourceData())
228                 targetPayload.add(delta.getTargetData())
229         }
230         return ['xpaths':xpaths, 'action':action, 'sourcePayload':sourcePayload, 'targetPayload':targetPayload]
231     }
232
233     def countDataNodesInBookstore() {
234         return countDataNodesInTree(cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', INCLUDE_ALL_DESCENDANTS))
235     }
236
237     def countTopLevelListDataNodesInBookstore() {
238         return countDataNodesInTree(cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/', INCLUDE_ALL_DESCENDANTS))
239     }
240
241     def countXmlDataNodesInBookstore() {
242         return countDataNodesInTree(cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_4, BOOKSTORE_ANCHOR_6, '/bookstore', INCLUDE_ALL_DESCENDANTS))
243     }
244
245 }