4823d58af9fdf65dd828c09431e30a3255003a5e
[cps.git] /
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2023-2024 Nordix Foundation
4  *  Modifications Copyright (C) 2023-2025 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.integration.functional.cps
23
24 import org.onap.cps.api.CpsDataService
25 import org.onap.cps.integration.base.FunctionalSpecBase
26 import org.onap.cps.api.parameters.FetchDescendantsOption
27 import org.onap.cps.api.exceptions.AlreadyDefinedException
28 import org.onap.cps.api.exceptions.AnchorNotFoundException
29 import org.onap.cps.api.exceptions.CpsAdminException
30 import org.onap.cps.api.exceptions.CpsPathException
31 import org.onap.cps.api.exceptions.DataNodeNotFoundException
32 import org.onap.cps.api.exceptions.DataNodeNotFoundExceptionBatch
33 import org.onap.cps.api.exceptions.DataValidationException
34 import org.onap.cps.api.exceptions.DataspaceNotFoundException
35 import org.onap.cps.api.model.DeltaReport
36 import org.onap.cps.utils.ContentType
37
38 import static org.onap.cps.api.parameters.FetchDescendantsOption.DIRECT_CHILDREN_ONLY
39 import static org.onap.cps.api.parameters.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
40 import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS
41
42 class DataServiceIntegrationSpec extends FunctionalSpecBase {
43
44     CpsDataService objectUnderTest
45     def originalCountBookstoreChildNodes
46     def originalCountXmlBookstoreChildNodes
47     def originalCountBookstoreTopLevelListNodes
48
49     def setup() {
50         objectUnderTest = cpsDataService
51         originalCountBookstoreChildNodes = countDataNodesInBookstore()
52         originalCountBookstoreTopLevelListNodes = countTopLevelListDataNodesInBookstore()
53         originalCountXmlBookstoreChildNodes = countXmlDataNodesInBookstore()
54     }
55
56     def 'Read bookstore top-level container(s) using #fetchDescendantsOption.'() {
57         when: 'get data nodes for bookstore container'
58             def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', fetchDescendantsOption)
59         then: 'the tree consist ouf of #expectNumberOfDataNodes data nodes'
60             assert countDataNodesInTree(result) == expectNumberOfDataNodes
61         and: 'the top level data node has the expected attribute and value'
62             assert result.leaves['bookstore-name'] == ['Easons-1']
63         and: 'they are from the correct dataspace'
64             assert result.dataspace == [FUNCTIONAL_TEST_DATASPACE_1]
65         and: 'they are from the correct anchor'
66             assert result.anchorName == [BOOKSTORE_ANCHOR_1]
67         where: 'the following option is used'
68             fetchDescendantsOption        || expectNumberOfDataNodes
69             OMIT_DESCENDANTS              || 1
70             DIRECT_CHILDREN_ONLY          || 7
71             INCLUDE_ALL_DESCENDANTS       || 28
72             new FetchDescendantsOption(2) || 28
73     }
74
75     def 'Read bookstore top-level container(s) using "root" path variations.'() {
76         when: 'get data nodes for bookstore container'
77             def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, root, OMIT_DESCENDANTS)
78         then: 'the tree consist correct number of data nodes'
79             assert countDataNodesInTree(result) == 2
80         and: 'the top level data node has the expected number of leaves'
81             assert result.leaves.size() == 2
82         where: 'the following variations of "root" are used'
83             root << [ '/', '' ]
84     }
85
86     def 'Read data nodes with error: #cpsPath'() {
87         when: 'attempt to get data nodes using invalid path'
88             objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, cpsPath, DIRECT_CHILDREN_ONLY)
89         then: 'a #expectedException is thrown'
90             thrown(expectedException)
91         where:
92             cpsPath              || expectedException
93             'invalid path'       || CpsPathException
94             '/non-existing-path' || DataNodeNotFoundException
95     }
96
97     def 'Read (multiple) data nodes (batch) with #cpsPath'() {
98         when: 'attempt to get data nodes using invalid path'
99             objectUnderTest.getDataNodesForMultipleXpaths(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, [ cpsPath ], DIRECT_CHILDREN_ONLY)
100         then: 'no exception is thrown'
101             noExceptionThrown()
102         where:
103             cpsPath << [ 'invalid path', '/non-existing-path' ]
104     }
105
106     def 'Get data nodes error scenario #scenario'() {
107         when: 'attempt to retrieve data nodes'
108             objectUnderTest.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS)
109         then: 'expected exception is thrown'
110             thrown(expectedException)
111         where: 'following data is used'
112             scenario                 | dataspaceName                | anchorName        | xpath           || expectedException
113             'non existent dataspace' | 'non-existent'               | 'not-relevant'    | '/not-relevant' || DataspaceNotFoundException
114             'non existent anchor'    | FUNCTIONAL_TEST_DATASPACE_1  | 'non-existent'    | '/not-relevant' || AnchorNotFoundException
115             'non-existent xpath'     | FUNCTIONAL_TEST_DATASPACE_1  | BOOKSTORE_ANCHOR_1| '/non-existing' || DataNodeNotFoundException
116             'invalid-dataspace'      | 'Invalid dataspace'          | 'not-relevant'    | '/not-relevant' || DataValidationException
117             'invalid-dataspace'      | FUNCTIONAL_TEST_DATASPACE_1  | 'Invalid Anchor'  | '/not-relevant' || DataValidationException
118     }
119
120     def 'Delete root data node.'() {
121         when: 'the "root" is deleted'
122             objectUnderTest.deleteDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, [ '/' ], now)
123         and: 'attempt to get the top level data node'
124             objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', DIRECT_CHILDREN_ONLY)
125         then: 'an datanode not found exception is thrown'
126             thrown(DataNodeNotFoundException)
127         cleanup:
128             restoreBookstoreDataAnchor(1)
129     }
130
131     def 'Get whole list data' () {
132             def xpathForWholeList = "/bookstore/categories"
133         when: 'get data nodes for bookstore container'
134             def dataNodes = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, xpathForWholeList, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
135         then: 'the tree consist ouf of #expectNumberOfDataNodes data nodes'
136             assert dataNodes.size() == 5
137         and: 'each datanode contains the list node xpath partially in its xpath'
138             dataNodes.each {dataNode ->
139                 assert dataNode.xpath.contains(xpathForWholeList)
140             }
141     }
142
143     def 'Read (multiple) data nodes with #scenario' () {
144         when: 'attempt to get data nodes using multiple valid xpaths'
145             def dataNodes = objectUnderTest.getDataNodesForMultipleXpaths(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, xpath, OMIT_DESCENDANTS)
146         then: 'expected numer of data nodes are returned'
147             dataNodes.size() == expectedNumberOfDataNodes
148         where: 'the following data was used'
149                     scenario                    |                       xpath                                       |   expectedNumberOfDataNodes
150             'container-node xpath'              | ['/bookstore']                                                    |               1
151             'list-item'                         | ['/bookstore/categories[@code=1]']                                |               1
152             'parent-list xpath'                 | ['/bookstore/categories']                                         |               5
153             'child-list xpath'                  | ['/bookstore/categories[@code=1]/books']                          |               2
154             'both parent and child list xpath'  | ['/bookstore/categories', '/bookstore/categories[@code=1]/books'] |               7
155     }
156
157     def 'Add and Delete a (container) data node using #scenario.'() {
158             when: 'the new datanode is saved'
159                 objectUnderTest.saveData(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , parentXpath, json, now)
160             then: 'it can be retrieved by its normalized xpath'
161                 def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, normalizedXpathToNode, DIRECT_CHILDREN_ONLY)
162                 assert result.size() == 1
163                 assert result[0].xpath == normalizedXpathToNode
164             and: 'there is now one extra datanode'
165                 assert originalCountBookstoreChildNodes + 1 == countDataNodesInBookstore()
166             when: 'the new datanode is deleted'
167                 objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, normalizedXpathToNode, now)
168             then: 'the original number of data nodes is restored'
169                 assert originalCountBookstoreChildNodes == countDataNodesInBookstore()
170             where:
171                 scenario                      | parentXpath                         | json                                                                                        || normalizedXpathToNode
172                 'normalized parent xpath'     | '/bookstore'                        | '{"webinfo": {"domain-name":"ourbookstore.com", "contact-email":"info@ourbookstore.com" }}' || "/bookstore/webinfo"
173                 'non-normalized parent xpath' | '/bookstore/categories[ @code="1"]' | '{"books": {"title":"new" }}'                                                               || "/bookstore/categories[@code='1']/books[@title='new']"
174     }
175
176     def 'Attempt to create a top level data node using root.'() {
177         given: 'a new anchor'
178             cpsAnchorService.createAnchor(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_SCHEMA_SET, 'newAnchor1');
179         when: 'attempt to save new top level datanode'
180             def json = '{"bookstore": {"bookstore-name": "New Store"} }'
181             objectUnderTest.saveData(FUNCTIONAL_TEST_DATASPACE_1, 'newAnchor1' , '/', json, now)
182         then: 'since there is no data a data node not found exception is thrown'
183             thrown(DataNodeNotFoundException)
184     }
185
186     def 'Attempt to save top level data node that already exist'() {
187         when: 'attempt to save already existing top level node'
188             def json = '{"bookstore": {} }'
189             objectUnderTest.saveData(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, json, now)
190         then: 'an exception that (one cps paths is)  already defined is thrown '
191             def exceptionThrown = thrown(AlreadyDefinedException)
192             exceptionThrown.alreadyDefinedObjectNames == ['/bookstore' ] as Set
193         cleanup:
194             restoreBookstoreDataAnchor(1)
195     }
196
197     def 'Delete a single datanode with invalid path.'() {
198         when: 'attempt to delete a single datanode with invalid path'
199             objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/invalid path', now)
200         then: 'a cps path parser exception is thrown'
201             thrown(CpsPathException)
202     }
203
204     def 'Delete multiple data nodes with invalid path.'() {
205         when: 'attempt to delete datanode collection with invalid path'
206             objectUnderTest.deleteDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, ['/invalid path'], now)
207         then: 'the error is silently ignored'
208             noExceptionThrown()
209     }
210
211     def 'Delete single data node with non-existing path.'() {
212         when: 'attempt to delete a single datanode non-existing invalid path'
213             objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/does/not/exist', now)
214         then: 'a datanode not found exception is thrown'
215             thrown(DataNodeNotFoundException)
216     }
217
218     def 'Delete multiple data nodes with non-existing path(s).'() {
219         when: 'attempt to delete a single datanode non-existing invalid path'
220             objectUnderTest.deleteDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, ['/does/not/exist'], now)
221         then: 'a  datanode not found (batch) exception is thrown'
222             thrown(DataNodeNotFoundExceptionBatch)
223     }
224
225     def 'Add and Delete top-level list (element) data nodes with root node.'() {
226         given: 'a new (multiple-data-tree:invoice) datanodes'
227             def json = '{"bookstore-address":[{"bookstore-name":"Easons","address":"Bangalore,India","postal-code":"560043"}]}'
228         when: 'the new list elements are saved'
229             objectUnderTest.saveListElements(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/', json, now, ContentType.JSON)
230         then: 'they can be retrieved by their xpaths'
231             objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore-address[@bookstore-name="Easons"]', INCLUDE_ALL_DESCENDANTS)
232         and: 'there is one extra datanode'
233             assert originalCountBookstoreTopLevelListNodes + 1 == countTopLevelListDataNodesInBookstore()
234         when: 'the new elements are deleted'
235             objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore-address[@bookstore-name="Easons"]', now)
236         then: 'the original number of datanodes is restored'
237             assert originalCountBookstoreTopLevelListNodes == countTopLevelListDataNodesInBookstore()
238     }
239
240     def 'Add and Delete list (element) data nodes.'() {
241         given: 'two new (categories) data nodes'
242             def json = '{"categories": [ {"code":"new1"}, {"code":"new2" } ] }'
243         when: 'the new list elements are saved'
244             objectUnderTest.saveListElements(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore', json, now, ContentType.JSON)
245         then: 'they can be retrieved by their xpaths'
246             objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/categories[@code="new1"]', DIRECT_CHILDREN_ONLY).size() == 1
247             objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/categories[@code="new2"]', DIRECT_CHILDREN_ONLY).size() == 1
248         and: 'there are now two extra data nodes'
249             assert originalCountBookstoreChildNodes + 2 == countDataNodesInBookstore()
250         when: 'the new elements are deleted'
251             objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/categories[@code="new1"]', now)
252             objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/categories[@code="new2"]', now)
253         then: 'the original number of data nodes is restored'
254             assert originalCountBookstoreChildNodes == countDataNodesInBookstore()
255     }
256
257     def 'Add list (element) data nodes that already exist.'() {
258         given: 'two (categories) data nodes, one new and one existing'
259             def json = '{"categories": [ {"code":"1"}, {"code":"new1"} ] }'
260         when: 'attempt to save the list element'
261             objectUnderTest.saveListElements(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore', json, now, ContentType.JSON)
262         then: 'an exception that (one cps paths is)  already defined is thrown '
263             def exceptionThrown = thrown(AlreadyDefinedException)
264             exceptionThrown.alreadyDefinedObjectNames == ['/bookstore/categories[@code=\'1\']' ] as Set
265         and: 'there is now one extra data nodes'
266             assert originalCountBookstoreChildNodes + 1 == countDataNodesInBookstore()
267         cleanup:
268             restoreBookstoreDataAnchor(1)
269     }
270
271     def 'Add and Delete list (element) data nodes using lists specific method.'() {
272         given: 'a new (categories) data nodes'
273             def json = '{"categories": [ {"code":"new1"} ] }'
274         and: 'the new list element is saved'
275             objectUnderTest.saveListElements(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore', json, now, ContentType.JSON)
276         when: 'the new element is deleted'
277             objectUnderTest.deleteListOrListElement(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/categories[@code="new1"]', now)
278         then: 'the original number of data nodes is restored'
279             assert originalCountBookstoreChildNodes == countDataNodesInBookstore()
280     }
281
282     def 'Add and Delete list (element) XML data nodes.'() {
283         given: 'a new (categories) data nodes in XML'
284             def xml = '<categories><code>new1</code><name>SciFii</name></categories>'
285         and: 'saving the new list element'
286             objectUnderTest.saveListElements(FUNCTIONAL_TEST_DATASPACE_4, BOOKSTORE_ANCHOR_6, '/bookstore', xml, now, ContentType.XML)
287         when: 'deleting the new list element'
288             objectUnderTest.deleteListOrListElement(FUNCTIONAL_TEST_DATASPACE_4, BOOKSTORE_ANCHOR_6, '/bookstore/categories[@code="new1"]', now)
289         then: 'the original number of data nodes is restored'
290             assert originalCountXmlBookstoreChildNodes == countXmlDataNodesInBookstore()
291     }
292
293     def 'Add and Delete a batch of list element data nodes.'() {
294         given: 'two new (categories) data nodes in a single batch'
295             def json = '{"categories": [ {"code":"new1"}, {"code":"new2"} ] }'
296         when: 'the batches of new list element(s) are saved'
297             objectUnderTest.saveListElements(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore', json, now, ContentType.JSON)
298         then: 'they can be retrieved by their xpaths'
299             assert objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/categories[@code="new1"]', DIRECT_CHILDREN_ONLY).size() == 1
300             assert objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/categories[@code="new2"]', DIRECT_CHILDREN_ONLY).size() == 1
301         and: 'there are now two extra data nodes'
302             assert originalCountBookstoreChildNodes + 2 == countDataNodesInBookstore()
303         when: 'the new elements are deleted'
304             objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/categories[@code="new1"]', now)
305             objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/categories[@code="new2"]', now)
306         then: 'the original number of data nodes is restored'
307             assert originalCountBookstoreChildNodes == countDataNodesInBookstore()
308     }
309
310     def 'Add and Delete a batch of list element data nodes with partial success.'() {
311         given: 'one existing and one new (categories) data nodes in a single batch'
312             def json = '{"categories": [ {"code":"new1"}, {"code":"1"} ] }'
313         when: 'the batches of new list element(s) are saved'
314             objectUnderTest.saveListElements(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore', json, now, ContentType.JSON)
315         then: 'an already defined (batch) exception is thrown for the existing path'
316             def exceptionThrown = thrown(AlreadyDefinedException)
317             assert exceptionThrown.alreadyDefinedObjectNames ==  ['/bookstore/categories[@code=\'1\']' ] as Set
318         and: 'there is now one extra data node'
319             assert originalCountBookstoreChildNodes + 1 == countDataNodesInBookstore()
320         cleanup:
321             restoreBookstoreDataAnchor(1)
322     }
323
324     def 'Attempt to add empty lists.'() {
325         when: 'the batches of new list element(s) are saved'
326             objectUnderTest.replaceListContent(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore', [ ] as String, now, ContentType.JSON)
327         then: 'an data exception is thrown'
328             thrown(DataValidationException)
329     }
330
331     def 'Add child error scenario: #scenario.'() {
332         when: 'attempt to add a child data node with #scenario'
333             objectUnderTest.saveData(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, parentXpath, json, now)
334         then: 'a #expectedException is thrown'
335             thrown(expectedException)
336         where: 'the following data is used'
337             scenario                 | parentXpath                              | json                                || expectedException
338             'parent does not exist'  | '/bookstore/categories[@code="unknown"]' | '{"books": [ {"title":"new"} ] } '  || DataNodeNotFoundException
339             'already existing child' | '/bookstore'                             | '{"categories": [ {"code":"1"} ] }' || AlreadyDefinedException
340     }
341
342     def 'Add multiple child data nodes with partial success.'() {
343         given: 'one existing and one new list element'
344             def json = '{"categories": [ {"code":"1"}, {"code":"new"} ] }'
345         when: 'attempt to add the elements'
346             objectUnderTest.saveData(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', json, now)
347         then: 'an already defined (batch) exception is thrown for the existing path'
348             def thrown  = thrown(AlreadyDefinedException)
349             assert thrown.alreadyDefinedObjectNames == [ "/bookstore/categories[@code='1']" ] as Set
350         and: 'the new data node has been added i.e. can be retrieved'
351             assert objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/categories[@code="new"]', DIRECT_CHILDREN_ONLY).size() == 1
352     }
353
354     def 'Replace list content #scenario.'() {
355         given: 'the bookstore categories 1 and 2 exist and have at least 1 child each '
356             assert countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/categories[@code="1"]', DIRECT_CHILDREN_ONLY)) > 1
357             assert countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/categories[@code="2"]', DIRECT_CHILDREN_ONLY)) > 1
358         when: 'the categories list is replaced with just category "1" and without child nodes (books)'
359             def json = '{"categories": [ {"code":"' +categoryCode + '"' + childJson + '} ] }'
360             objectUnderTest.replaceListContent(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', json, now, ContentType.JSON)
361         then: 'the new replaced category can be retrieved but has no children anymore'
362             assert expectedNumberOfDataNodes == countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/categories[@code="' +categoryCode + '"]', DIRECT_CHILDREN_ONLY))
363         when: 'attempt to retrieve a category (code) not in the new list'
364             objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/categories[@code="2"]', DIRECT_CHILDREN_ONLY)
365         then: 'a datanode not found exception occurs'
366             thrown(DataNodeNotFoundException)
367         cleanup:
368             restoreBookstoreDataAnchor(1)
369         where: 'the following data is used'
370             scenario                        | categoryCode | childJson                                 || expectedNumberOfDataNodes
371             'existing code, no children'    | '1'          | ''                                        || 1
372             'existing code, new child'      | '1'          | ', "books" : [ { "title": "New Book" } ]' || 2
373             'existing code, existing child' | '1'          | ', "books" : [ { "title": "Matilda" } ]'  || 2
374             'new code, new child'           | 'new'        | ', "books" : [ { "title": "New Book" } ]' || 2
375     }
376
377     def 'Replace XML list content #scenario.'() {
378         given: 'the XML bookstore categories 1 exists and has at least 1 child'
379             assert countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_4, BOOKSTORE_ANCHOR_6, '/bookstore/categories[@code=1]', DIRECT_CHILDREN_ONLY)) > 1
380         when: 'the XML categories list is replaced with just category "1" and a new child'
381             def xmlData = '''<bookstore xmlns="org:onap:cps:sample">
382                               <categories>
383                                   <code>1</code>
384                                   <books>
385                                   <title>New Book</title>
386                                   </books>
387                               </categories>
388                            </bookstore>'''
389             objectUnderTest.replaceListContent(FUNCTIONAL_TEST_DATASPACE_4, BOOKSTORE_ANCHOR_6, '/bookstore', xmlData, now, ContentType.XML)
390         then: 'the replaced XML category has one child'
391             assert 2 == countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_4, BOOKSTORE_ANCHOR_6, '/bookstore/categories[@code=1]', DIRECT_CHILDREN_ONLY))
392         when: 'attempting to retrieve a non-existent category'
393             objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_4, BOOKSTORE_ANCHOR_6, '/bookstore/categories[@code="2"]', DIRECT_CHILDREN_ONLY)
394         then: 'a datanode not found exception occurs'
395             thrown(DataNodeNotFoundException)
396         cleanup:
397             restoreBookstoreDataAnchor(1)
398     }
399
400     def 'Update data node leaves for node that has no leaves (yet).'() {
401         given: 'new (webinfo) datanode without leaves'
402             def json = '{"webinfo": {} }'
403             objectUnderTest.saveData(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore', json, now)
404         when: 'update is performed to add a leaf'
405             def updatedJson = '{"webinfo": {"domain-name":"new leaf data"}}'
406             objectUnderTest.updateNodeLeaves(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, "/bookstore", updatedJson, now, ContentType.JSON)
407         then: 'the updated data nodes are retrieved'
408             def result = cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, "/bookstore/webinfo", INCLUDE_ALL_DESCENDANTS)
409         and: 'the leaf value is updated as expected'
410             assert result.leaves['domain-name'] == ['new leaf data']
411         cleanup:
412             restoreBookstoreDataAnchor(1)
413     }
414
415     def 'Update multiple data leaves error scenario: #scenario.'() {
416         when: 'attempt to update data node for #scenario'
417             objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, xpath, 'irrelevant json data', now, ContentType.JSON)
418         then: 'a #expectedException is thrown'
419             thrown(expectedException)
420         where: 'the following data is used'
421             scenario                 | dataspaceName               | anchorName                 | xpath           || expectedException
422             'invalid dataspace name' | 'Invalid Dataspace'         | 'not-relevant'             | '/not relevant' || DataValidationException
423             'invalid anchor name'    | FUNCTIONAL_TEST_DATASPACE_1 | 'INVALID ANCHOR'           | '/not relevant' || DataValidationException
424             'non-existing dataspace' | 'non-existing-dataspace'    | 'not-relevant'             | '/not relevant' || DataspaceNotFoundException
425             'non-existing anchor'    | FUNCTIONAL_TEST_DATASPACE_1 | 'non-existing-anchor'      | '/not relevant' || AnchorNotFoundException
426             'non-existing-xpath'     | FUNCTIONAL_TEST_DATASPACE_1 | BOOKSTORE_ANCHOR_1         | '/non-existing' || DataValidationException
427     }
428
429     def 'Update data nodes and descendants.'() {
430         given: 'some web info for the bookstore'
431             def json = '{"webinfo": {"domain-name":"ourbookstore.com" ,"contact-email":"info@ourbookstore.com" }}'
432             objectUnderTest.saveData(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore', json, now)
433         when: 'the webinfo (container) is updated'
434             json = '{"webinfo": {"domain-name":"newdomain.com" ,"contact-email":"info@newdomain.com" }}'
435             objectUnderTest.updateDataNodeAndDescendants(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', json, now, ContentType.JSON)
436         then: 'webinfo has been updated with teh new details'
437             def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/webinfo', DIRECT_CHILDREN_ONLY)
438             result.leaves.'domain-name'[0] == 'newdomain.com'
439             result.leaves.'contact-email'[0] == 'info@newdomain.com'
440         cleanup:
441             restoreBookstoreDataAnchor(1)
442     }
443
444     def 'Update bookstore top-level container data node.'() {
445         when: 'the bookstore top-level container is updated'
446             def json = '{ "bookstore": { "bookstore-name": "new bookstore" }}'
447             objectUnderTest.updateDataNodeAndDescendants(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/', json, now, ContentType.JSON)
448         then: 'bookstore name has been updated'
449             def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', DIRECT_CHILDREN_ONLY)
450             result.leaves.'bookstore-name'[0] == 'new bookstore'
451         cleanup:
452             restoreBookstoreDataAnchor(1)
453     }
454
455     def 'Update multiple data node leaves.'() {
456         given: 'Updated json for bookstore data'
457             def jsonData =  "{'book-store:books':{'lang':'English/French','price':100,'title':'Matilda'}}"
458         when: 'update is performed for leaves'
459             objectUnderTest.updateNodeLeaves(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code='1']", jsonData, now, ContentType.JSON)
460         then: 'the updated data nodes are retrieved'
461             def result = cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code=1]/books[@title='Matilda']", INCLUDE_ALL_DESCENDANTS)
462         and: 'the leaf values are updated as expected'
463             assert result[0].leaves['lang'] == 'English/French'
464             assert result[0].leaves['price'] == 100
465         cleanup:
466             restoreBookstoreDataAnchor(2)
467     }
468
469     def 'Update bookstore top-level XML container data node.'() {
470         given: 'Updated xml for bookstore data'
471             def xml = '<categories><code>1</code><name>Gothic</name></categories>'
472         when: 'updating the data node'
473             objectUnderTest.updateDataNodeAndDescendants(FUNCTIONAL_TEST_DATASPACE_4, BOOKSTORE_ANCHOR_6, '/bookstore', xml, now, ContentType.XML)
474         then: 'the updated data nodes are retrieved'
475             def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_4, BOOKSTORE_ANCHOR_6, '/bookstore/categories[@code=1]', DIRECT_CHILDREN_ONLY)
476         and: 'the leaf values are updated as expected'
477             assert result.leaves.'name'[0] == 'Gothic'
478         cleanup:
479             restoreBookstoreXmlDataAnchor(1)
480     }
481
482     def 'Update multiple XML data node leaves.'() {
483         given: 'XML for bookstore data with updated lang and price leaves'
484             def xmlData = '''<books>
485                       <title>2001: A Space Odyssey</title>
486                       <lang>english</lang>
487                       <authors>Iain M. Banks</authors>
488                       <editions>1994</editions>
489                       <price>995</price>
490                       </books> '''
491         when: 'updating node leaves'
492             objectUnderTest.updateNodeLeaves(FUNCTIONAL_TEST_DATASPACE_4, BOOKSTORE_ANCHOR_6, '/bookstore/categories[@code=1]', xmlData, now, ContentType.XML)
493         then: 'the updated data nodes are retrieved'
494             def result = cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_4, BOOKSTORE_ANCHOR_6, '/bookstore/categories[@code=1]/books[@title="2001: A Space Odyssey"]', INCLUDE_ALL_DESCENDANTS)
495         and: 'the leaf values are updated as expected'
496             assert result[0].leaves['lang'] == 'english'
497             assert result[0].leaves['price'] == 995
498     }
499
500     def 'Order of leaf-list elements is preserved when "ordered-by user" is set in the YANG model.'() {
501         given: 'Updated json for bookstore data'
502             def jsonData =  "{'book-store:books':{'title':'Matilda', 'authors': ['beta', 'alpha', 'gamma', 'delta']}}"
503         when: 'update is performed for leaves'
504             objectUnderTest.updateNodeLeaves(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code='1']", jsonData, now, ContentType.JSON)
505         and: 'the updated data nodes are retrieved'
506             def result = cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code=1]/books[@title='Matilda']", INCLUDE_ALL_DESCENDANTS)
507         then: 'the leaf-list values have expected order'
508             assert result[0].leaves['authors'] == ['beta', 'alpha', 'gamma', 'delta']
509         cleanup:
510             restoreBookstoreDataAnchor(2)
511     }
512
513     def 'Leaf-list elements are sorted when "ordered-by user" is not set in the YANG model.'() {
514         given: 'Updated json for bookstore data'
515             def jsonData =  "{'book-store:books':{'title':'Matilda', 'editions': [2011, 1988, 2001, 2022, 2025]}}"
516         when: 'update is performed for leaves'
517             objectUnderTest.updateNodeLeaves(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code='1']", jsonData, now, ContentType.JSON)
518         and: 'the updated data nodes are retrieved'
519             def result = cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, "/bookstore/categories[@code=1]/books[@title='Matilda']", INCLUDE_ALL_DESCENDANTS)
520         then: 'the leaf-list values have natural order'
521             assert result[0].leaves['editions'] == [1988, 2001, 2011, 2022, 2025]
522         cleanup:
523             restoreBookstoreDataAnchor(2)
524     }
525
526     def 'Get delta between 2 anchors'() {
527         when: 'attempt to get delta report between anchors'
528             def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, '/', OMIT_DESCENDANTS)
529         and: 'report is ordered based on xpath'
530             result = result.toList().sort { it.xpath }
531         then: 'delta report contains expected number of changes'
532             result.size() == 3
533         and: 'delta report contains REPLACE action with expected xpath'
534             assert result[0].getAction() == 'replace'
535             assert result[0].getXpath() == '/bookstore'
536         and: 'delta report contains CREATE action with expected xpath'
537             assert result[1].getAction() == 'create'
538             assert result[1].getXpath() == "/bookstore-address[@bookstore-name='Crossword Bookstores']"
539         and: 'delta report contains REMOVE action with expected xpath'
540             assert result[2].getAction() == 'remove'
541             assert result[2].getXpath() == "/bookstore-address[@bookstore-name='Easons-1']"
542     }
543
544     def 'Get delta between 2 anchors returns empty response when #scenario'() {
545         when: 'attempt to get delta report between anchors'
546             def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, targetAnchor, xpath, INCLUDE_ALL_DESCENDANTS)
547         then: 'delta report is empty'
548             assert result.isEmpty()
549         where: 'following data was used'
550             scenario                              | targetAnchor       | xpath
551         'anchors with identical data are queried' | BOOKSTORE_ANCHOR_4 | '/'
552         'same anchor name is passed as parameter' | BOOKSTORE_ANCHOR_3 | '/'
553         'non existing xpath'                      | BOOKSTORE_ANCHOR_5 | '/non-existing-xpath'
554     }
555
556     def 'Get delta between anchors error scenario: #scenario'() {
557         when: 'attempt to get delta between anchors'
558             objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, sourceAnchor, targetAnchor, '/some-xpath', INCLUDE_ALL_DESCENDANTS)
559         then: 'expected exception is thrown'
560             thrown(expectedException)
561         where: 'following data was used'
562                     scenario                               | dataspaceName               | sourceAnchor          | targetAnchor          || expectedException
563             'invalid dataspace name'                       | 'Invalid dataspace'         | 'not-relevant'        | 'not-relevant'        || DataValidationException
564             'invalid anchor 1 name'                        | FUNCTIONAL_TEST_DATASPACE_3 | 'invalid anchor'      | 'not-relevant'        || DataValidationException
565             'invalid anchor 2 name'                        | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3    | 'invalid anchor'      || DataValidationException
566             'non-existing dataspace'                       | 'non-existing'              | 'not-relevant1'       | 'not-relevant2'       || DataspaceNotFoundException
567             'non-existing dataspace with same anchor name' | 'non-existing'              | 'not-relevant'        | 'not-relevant'        || DataspaceNotFoundException
568             'non-existing anchor 1'                        | FUNCTIONAL_TEST_DATASPACE_3 | 'non-existing-anchor' | 'not-relevant'        || AnchorNotFoundException
569             'non-existing anchor 2'                        | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3    | 'non-existing-anchor' || AnchorNotFoundException
570     }
571
572     def 'Get delta between anchors for remove action, where source data node #scenario'() {
573         when: 'attempt to get delta between leaves of data nodes present in 2 anchors'
574             def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_5, BOOKSTORE_ANCHOR_3, parentNodeXpath, INCLUDE_ALL_DESCENDANTS)
575         then: 'expected action is present in delta report'
576             assert result.get(0).getAction() == 'remove'
577         where: 'following data was used'
578             scenario                     | parentNodeXpath
579             'has leaves and child nodes' | "/bookstore/categories[@code='6']"
580             'has leaves only'            | "/bookstore/categories[@code='5']/books[@title='Book 11']"
581             'has child data node only'   | "/bookstore/support-info/contact-emails"
582             'is empty'                   | "/bookstore/container-without-leaves"
583     }
584
585     def 'Get delta between anchors for "create" action, where target data node #scenario'() {
586         when: 'attempt to get delta between leaves of data nodes present in 2 anchors'
587             def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, parentNodeXpath, INCLUDE_ALL_DESCENDANTS)
588         then: 'the expected action is present in delta report'
589             result.get(0).getAction() == 'create'
590         and: 'the expected xapth is present in delta report'
591             result.get(0).getXpath() == parentNodeXpath
592         where: 'following data was used'
593             scenario                     | parentNodeXpath
594             'has leaves and child nodes' | "/bookstore/categories[@code='6']"
595             'has leaves only'            | "/bookstore/categories[@code='5']/books[@title='Book 11']"
596             'has child data node only'   | "/bookstore/support-info/contact-emails"
597             'is empty'                   | "/bookstore/container-without-leaves"
598     }
599
600     def 'Get delta between anchors when leaves of existing data nodes are updated,: #scenario'() {
601         when: 'attempt to get delta between leaves of existing data nodes'
602             def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, sourceAnchor, targetAnchor, xpath, OMIT_DESCENDANTS)
603         then: 'expected action is "replace"'
604             assert result[0].getAction() == 'replace'
605         and: 'the payload has expected leaf values'
606             def sourceData = result[0].getSourceData()
607             def targetData = result[0].getTargetData()
608             assert sourceData == expectedSourceValue
609             assert targetData == expectedTargetValue
610         where: 'following data was used'
611             scenario                           | sourceAnchor       | targetAnchor       | xpath                                                     || expectedSourceValue            | expectedTargetValue
612             'leaf is updated in target anchor' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore'                                              || ['bookstore-name': 'Easons-1'] | ['bookstore-name': 'Crossword Bookstores']
613             'leaf is removed in target anchor' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | "/bookstore/categories[@code='5']/books[@title='Book 1']" || [price:1]                      | null
614             'leaf is added in target anchor'   | BOOKSTORE_ANCHOR_5 | BOOKSTORE_ANCHOR_3 | "/bookstore/categories[@code='5']/books[@title='Book 1']" || null                           | [price:1]
615     }
616
617     def 'Get delta between anchors when child data nodes under existing parent data nodes are updated: #scenario'() {
618         when: 'attempt to get delta between leaves of existing data nodes'
619             def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, sourceAnchor, targetAnchor, xpath, DIRECT_CHILDREN_ONLY)
620         then: 'expected action is "replace"'
621             assert result[0].getAction() == 'replace'
622         and: 'the delta report has expected child node xpaths'
623             def deltaReportEntities = getDeltaReportEntities(result)
624             def childNodeXpathsInDeltaReport = deltaReportEntities.get('xpaths')
625             assert childNodeXpathsInDeltaReport.contains(expectedChildNodeXpath)
626         where: 'following data was used'
627             scenario                                          | sourceAnchor       | targetAnchor       | xpath                 || expectedChildNodeXpath
628             '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\']'
629             'removed child data nodes in target anchor'       | BOOKSTORE_ANCHOR_5 | BOOKSTORE_ANCHOR_3 | '/bookstore'          || '/bookstore/support-info'
630             'added  child data nodes in target anchor'        | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore'          || '/bookstore/support-info'
631     }
632
633     def 'Get delta between anchors where source and target data nodes have leaves and child data nodes'() {
634         given: 'parent node xpath and expected data in delta report'
635             def parentNodeXpath = "/bookstore/categories[@code='1']"
636             def expectedSourceDataInParentNode = ['name':'Children']
637             def expectedTargetDataInParentNode = ['name':'Kids']
638             def expectedSourceDataInChildNode = [['lang' : 'English'],['price':20, 'editions':[1988, 2000]]]
639             def expectedTargetDataInChildNode = [['lang':'English/German'], ['price':200, 'editions':[1988, 2000, 2023]]]
640         when: 'attempt to get delta between leaves of existing data nodes'
641             def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, parentNodeXpath, INCLUDE_ALL_DESCENDANTS)
642             def deltaReportEntities = getDeltaReportEntities(result)
643         then: 'expected action is "replace"'
644             assert result[0].getAction() == 'replace'
645         and: 'the payload has expected parent node xpath'
646             assert deltaReportEntities.get('xpaths').contains(parentNodeXpath)
647         and: 'delta report has expected source and target data'
648             assert deltaReportEntities.get('sourcePayload').contains(expectedSourceDataInParentNode)
649             assert deltaReportEntities.get('targetPayload').contains(expectedTargetDataInParentNode)
650         and: 'the delta report also has expected child node xpaths'
651             assert deltaReportEntities.get('xpaths').containsAll(["/bookstore/categories[@code='1']/books[@title='The Gruffalo']", "/bookstore/categories[@code='1']/books[@title='Matilda']"])
652         and: 'the delta report also has expected source and target data of child nodes'
653             assert deltaReportEntities.get('sourcePayload').containsAll(expectedSourceDataInChildNode)
654             assert deltaReportEntities.get('targetPayload').containsAll(expectedTargetDataInChildNode)
655     }
656
657     def 'Get delta between anchor and JSON payload'() {
658         when: 'attempt to get delta report between anchor and JSON payload'
659             def jsonPayload = "{\"book-store:bookstore\":{\"bookstore-name\":\"Crossword Bookstores\"},\"book-store:bookstore-address\":{\"address\":\"Bangalore, India\",\"postal-code\":\"560062\",\"bookstore-name\":\"Crossword Bookstores\"}}"
660             def result = objectUnderTest.getDeltaByDataspaceAnchorAndPayload(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, '/', [:], jsonPayload, OMIT_DESCENDANTS)
661         then: 'delta report contains expected number of changes'
662             result.size() == 3
663         and: 'delta report contains "replace" action with expected xpath'
664             assert result[0].getAction() == 'replace'
665             assert result[0].getXpath() == '/bookstore'
666         and: 'delta report contains "remove" action with expected xpath'
667             assert result[1].getAction() == 'remove'
668             assert result[1].getXpath() == "/bookstore-address[@bookstore-name='Easons-1']"
669         and: 'delta report contains "create" action with expected xpath'
670             assert result[2].getAction() == 'create'
671             assert result[2].getXpath() == "/bookstore-address[@bookstore-name='Crossword Bookstores']"
672     }
673
674     def 'Get delta between anchor and payload returns empty response when JSON payload is identical to anchor data'() {
675         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)'
676             def jsonPayload = readResourceDataFile('bookstore/bookstoreData.json').replace('Easons', 'Easons-1')
677             def result = objectUnderTest.getDeltaByDataspaceAnchorAndPayload(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, '/', [:], jsonPayload, INCLUDE_ALL_DESCENDANTS)
678         then: 'delta report is empty'
679             assert result.isEmpty()
680     }
681
682     def 'Get delta between anchor and payload error scenario: #scenario'() {
683         when: 'attempt to get delta between anchor and json payload'
684             objectUnderTest.getDeltaByDataspaceAnchorAndPayload(dataspaceName, sourceAnchor, xpath, [:], jsonPayload, INCLUDE_ALL_DESCENDANTS)
685         then: 'expected exception is thrown'
686             thrown(expectedException)
687         where: 'following data was used'
688                 scenario                               | dataspaceName               | sourceAnchor          | xpath        | jsonPayload   || expectedException
689         'invalid dataspace name'                       | 'Invalid dataspace'         | 'not-relevant'        | '/'          | '{some-json}' || DataValidationException
690         'invalid anchor name'                          | FUNCTIONAL_TEST_DATASPACE_3 | 'invalid anchor'      | '/'          | '{some-json}' || DataValidationException
691         'non-existing dataspace'                       | 'non-existing'              | 'not-relevant'        | '/'          | '{some-json}' || DataspaceNotFoundException
692         'non-existing anchor'                          | FUNCTIONAL_TEST_DATASPACE_3 | 'non-existing-anchor' | '/'          | '{some-json}' || AnchorNotFoundException
693         'empty json payload with root node xpath'      | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3    | '/'          | ''            || DataValidationException
694         'empty json payload with non-root node xpath'  | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3    | '/bookstore' | ''            || DataValidationException
695     }
696
697     def getDeltaReportEntities(List<DeltaReport> deltaReport) {
698         def xpaths = []
699         def action = []
700         def sourcePayload = []
701         def targetPayload = []
702         deltaReport.each {
703             delta -> xpaths.add(delta.getXpath())
704                 action.add(delta.getAction())
705                 sourcePayload.add(delta.getSourceData())
706                 targetPayload.add(delta.getTargetData())
707         }
708         return ['xpaths':xpaths, 'action':action, 'sourcePayload':sourcePayload, 'targetPayload':targetPayload]
709     }
710
711     def countDataNodesInBookstore() {
712         return countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', INCLUDE_ALL_DESCENDANTS))
713     }
714
715     def countTopLevelListDataNodesInBookstore() {
716         return countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/', INCLUDE_ALL_DESCENDANTS))
717     }
718
719     def countXmlDataNodesInBookstore() {
720         return countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_4, BOOKSTORE_ANCHOR_6, '/bookstore', INCLUDE_ALL_DESCENDANTS))
721     }
722 }