5890450a4e9b6c04d5a4fb2920f6fba4f8ca508e
[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     def NO_GROUPING = false
41
42     def setup() {
43         objectUnderTest = cpsDeltaService
44         originalCountBookstoreChildNodes = countDataNodesInBookstore()
45         originalCountBookstoreTopLevelListNodes = countTopLevelListDataNodesInBookstore()
46         originalCountXmlBookstoreChildNodes = countXmlDataNodesInBookstore()
47     }
48
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'
55             result.size() == 3
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\']'
65     }
66
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'
77     }
78
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
93     }
94
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'
106     }
107
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'
121     }
122
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]
138     }
139
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'
154     }
155
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)
178     }
179
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'
185             result.size() == 3
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\']'
195     }
196
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()
203     }
204
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
218     }
219
220     def getDeltaReportEntities(List<DeltaReport> deltaReport) {
221         def xpaths = []
222         def action = []
223         def sourcePayload = []
224         def targetPayload = []
225         deltaReport.each {
226             delta -> xpaths.add(delta.getXpath())
227                 action.add(delta.getAction())
228                 sourcePayload.add(delta.getSourceData())
229                 targetPayload.add(delta.getTargetData())
230         }
231         return ['xpaths':xpaths, 'action':action, 'sourcePayload':sourcePayload, 'targetPayload':targetPayload]
232     }
233
234     def countDataNodesInBookstore() {
235         return countDataNodesInTree(cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', INCLUDE_ALL_DESCENDANTS))
236     }
237
238     def countTopLevelListDataNodesInBookstore() {
239         return countDataNodesInTree(cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/', INCLUDE_ALL_DESCENDANTS))
240     }
241
242     def countXmlDataNodesInBookstore() {
243         return countDataNodesInTree(cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_4, BOOKSTORE_ANCHOR_6, '/bookstore', INCLUDE_ALL_DESCENDANTS))
244     }
245
246 }