From 0339c71815a4ca4cbab3d263d6c4586a112cda64 Mon Sep 17 00:00:00 2001 From: Arpit Singh Date: Wed, 2 Aug 2023 18:35:31 +0530 Subject: CPS Delta API 1: Delta between 2 anchors - CPS Delta Feature Part 1: To find delta between two anchors - created new endpoint deltaByDataspaceAndAnchors - endpoint to take dataspaceName, source anchor, target anchor, xpath, descendants as input - added new service CpsDeltaService - added method to find delta between DataNodes: getDeltaReport - added method to find removed data nodes: getRemovedDeltaReports - added method to get Added DataNodes: getAddedDeltaReports - added method to get Map of xpath to DataNode: convertToXPathToDataNodesMap - added a POJO for delta report - Added new JSON data for delta feature testing - Added groovy test files CpsDeltaServiceImplSpec and DeltaReportBuilderSpec - code related to update operation, will be added in separate commit Issue-ID: CPS-1824 Signed-off-by: Arpit Singh Change-Id: I313f0f71d04b03878be7643f709d8af1aa6df6ba --- .../cps/integration/base/FunctionalSpecBase.groovy | 17 ++ .../CpsDataServiceIntegrationSpec.groovy | 97 +++++++++++ .../test/resources/data/bookstore/bookstore.yang | 11 ++ .../bookstore/bookstoreDataForDeltaReport.json | 192 +++++++++++++++++++++ 4 files changed, 317 insertions(+) create mode 100644 integration-test/src/test/resources/data/bookstore/bookstoreDataForDeltaReport.json (limited to 'integration-test') diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy index 327a39ee4..14612d6c1 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2023 Nordix Foundation + * Modifications Copyright (C) 2022-2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. @@ -26,17 +27,24 @@ class FunctionalSpecBase extends CpsIntegrationSpecBase { def static FUNCTIONAL_TEST_DATASPACE_1 = 'functionalTestDataspace1' def static FUNCTIONAL_TEST_DATASPACE_2 = 'functionalTestDataspace2' + def static FUNCTIONAL_TEST_DATASPACE_3 = 'functionalTestDataspace3' def static NUMBER_OF_ANCHORS_PER_DATASPACE_WITH_BOOKSTORE_DATA = 2 + def static NUMBER_OF_ANCHORS_PER_DATASPACE_WITH_BOOKSTORE_DELTA_DATA = 1 def static BOOKSTORE_ANCHOR_1 = 'bookstoreAnchor1' def static BOOKSTORE_ANCHOR_2 = 'bookstoreAnchor2' + def static BOOKSTORE_ANCHOR_3 = 'bookstoreSourceAnchor1' + def static BOOKSTORE_ANCHOR_4 = 'copyOfSourceAnchor1' + def static BOOKSTORE_ANCHOR_5 = 'bookstoreAnchorForDeltaReport1' def static initialized = false def static bookstoreJsonData = readResourceDataFile('bookstore/bookstoreData.json') + def static bookstoreJsonDataForDeltaReport = readResourceDataFile('bookstore/bookstoreDataForDeltaReport.json') def setup() { if (!initialized) { setupBookstoreInfraStructure() addBookstoreData() + addDeltaData() initialized = true } } @@ -44,9 +52,12 @@ class FunctionalSpecBase extends CpsIntegrationSpecBase { def setupBookstoreInfraStructure() { cpsAdminService.createDataspace(FUNCTIONAL_TEST_DATASPACE_1) cpsAdminService.createDataspace(FUNCTIONAL_TEST_DATASPACE_2) + cpsAdminService.createDataspace(FUNCTIONAL_TEST_DATASPACE_3) def bookstoreYangModelAsString = readResourceDataFile('bookstore/bookstore.yang') cpsModuleService.createSchemaSet(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_SCHEMA_SET, [bookstore: bookstoreYangModelAsString]) cpsModuleService.createSchemaSet(FUNCTIONAL_TEST_DATASPACE_2, BOOKSTORE_SCHEMA_SET, [bookstore: bookstoreYangModelAsString]) + cpsModuleService.createSchemaSet(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_SCHEMA_SET, [bookstore: bookstoreYangModelAsString]) + } def addBookstoreData() { @@ -54,6 +65,12 @@ class FunctionalSpecBase extends CpsIntegrationSpecBase { addAnchorsWithData(NUMBER_OF_ANCHORS_PER_DATASPACE_WITH_BOOKSTORE_DATA, FUNCTIONAL_TEST_DATASPACE_2, BOOKSTORE_SCHEMA_SET, 'bookstoreAnchor', bookstoreJsonData) } + def addDeltaData() { + addAnchorsWithData(NUMBER_OF_ANCHORS_PER_DATASPACE_WITH_BOOKSTORE_DELTA_DATA, FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_SCHEMA_SET, 'bookstoreSourceAnchor', bookstoreJsonData) + addAnchorsWithData(NUMBER_OF_ANCHORS_PER_DATASPACE_WITH_BOOKSTORE_DELTA_DATA, FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_SCHEMA_SET, 'copyOfSourceAnchor', bookstoreJsonData) + addAnchorsWithData(NUMBER_OF_ANCHORS_PER_DATASPACE_WITH_BOOKSTORE_DELTA_DATA, FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_SCHEMA_SET, 'bookstoreAnchorForDeltaReport', bookstoreJsonDataForDeltaReport) + } + def restoreBookstoreDataAnchor(anchorNumber) { def anchorName = 'bookstoreAnchor' + anchorNumber cpsAdminService.deleteAnchor(FUNCTIONAL_TEST_DATASPACE_1, anchorName) diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy index 12c97ed40..017ede7de 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy @@ -32,6 +32,7 @@ import org.onap.cps.spi.exceptions.DataNodeNotFoundException import org.onap.cps.spi.exceptions.DataNodeNotFoundExceptionBatch import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.spi.exceptions.DataspaceNotFoundException +import org.onap.cps.spi.model.DeltaReport import java.time.OffsetDateTime @@ -432,6 +433,102 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase { restoreBookstoreDataAnchor(2) } + def 'Get delta between 2 anchors for when #scenario'() { + when: 'attempt to get delta report between anchors' + def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, xpath, fetchDescendantOption) + then: 'delta report contains expected number of changes' + result.size() == 2 + and: 'delta report contains expected action' + assert result.get(index).getAction() == expectedActions + and: 'delta report contains expected xpath' + assert result.get(index).getXpath() == expectedXpath + where: 'following data was used' + scenario | index | xpath || expectedActions || expectedXpath | fetchDescendantOption + 'a node is removed' | 0 | '/' || 'remove' || "/bookstore-address[@bookstore-name='Easons-1']" | OMIT_DESCENDANTS + 'a node is added' | 1 | '/' || 'add' || "/bookstore-address[@bookstore-name='Crossword Bookstores']" | OMIT_DESCENDANTS + } + + def 'Get delta between 2 anchors where child nodes are added/removed but parent node remains unchanged'() { + def parentNodeXpath = "/bookstore" + when: 'attempt to get delta report between anchors' + def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, parentNodeXpath, INCLUDE_ALL_DESCENDANTS) + then: 'delta report contains expected number of changes' + result.size() == 11 + and: 'the delta report does not contain parent node xpath' + def xpaths = getDeltaReportEntities(result).get('xpaths') + assert !(xpaths.contains(parentNodeXpath)) + } + + def 'Get delta between 2 anchors returns empty response when #scenario'() { + when: 'attempt to get delta report between anchors' + def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, sourceAnchor, targetAnchor, xpath, INCLUDE_ALL_DESCENDANTS) + then: 'delta report is empty' + assert result.isEmpty() + where: 'following data was used' + scenario | sourceAnchor | targetAnchor | xpath + 'anchors with identical data are queried' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_4 | '/' + 'same anchor name is passed as parameter' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_3 | '/' + 'non existing xpath' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/non-existing-xpath' + } + + def 'Get delta between anchors error scenario: #scenario'() { + when: 'attempt to get delta between anchors' + objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, sourceAnchor, targetAnchor, '/some-xpath', INCLUDE_ALL_DESCENDANTS) + then: 'expected exception is thrown' + thrown(expectedException) + where: 'following data was used' + scenario | dataspaceName | sourceAnchor | targetAnchor || expectedException + 'invalid dataspace name' | 'Invalid dataspace' | 'not-relevant' | 'not-relevant' || DataValidationException + 'invalid anchor 1 name' | FUNCTIONAL_TEST_DATASPACE_3 | 'invalid anchor' | 'not-relevant' || DataValidationException + 'invalid anchor 2 name' | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3 | 'invalid anchor' || DataValidationException + 'non-existing dataspace' | 'non-existing' | 'not-relevant1' | 'not-relevant2' || DataspaceNotFoundException + 'non-existing dataspace with same anchor name' | 'non-existing' | 'not-relevant' | 'not-relevant' || DataspaceNotFoundException + 'non-existing anchor 1' | FUNCTIONAL_TEST_DATASPACE_3 | 'non-existing-anchor' | 'not-relevant' || AnchorNotFoundException + 'non-existing anchor 2' | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3 | 'non-existing-anchor' || AnchorNotFoundException + } + + def 'Get delta between anchors for remove action, where source data node #scenario'() { + when: 'attempt to get delta between leaves of data nodes present in 2 anchors' + def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_5, BOOKSTORE_ANCHOR_3, parentNodeXpath, INCLUDE_ALL_DESCENDANTS) + then: 'expected action is present in delta report' + assert result.get(0).getAction() == 'remove' + where: 'following data was used' + scenario | parentNodeXpath + 'has leaves and child nodes' | "/bookstore/categories[@code='6']" + 'has leaves only' | "/bookstore/categories[@code='5']/books[@title='Book 11']" + 'has child data node only' | "/bookstore/support-info/contact-emails" + 'is empty' | "/bookstore/container-without-leaves" + } + + def 'Get delta between anchors for add action, where target data node #scenario'() { + when: 'attempt to get delta between leaves of data nodes present in 2 anchors' + def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, parentNodeXpath, INCLUDE_ALL_DESCENDANTS) + then: 'the expected action is present in delta report' + result.get(0).getAction() == 'add' + and: 'the expected xapth is present in delta report' + result.get(0).getXpath() == parentNodeXpath + where: 'following data was used' + scenario | parentNodeXpath + 'has leaves and child nodes' | "/bookstore/categories[@code='6']" + 'has leaves only' | "/bookstore/categories[@code='5']/books[@title='Book 11']" + 'has child data node only' | "/bookstore/support-info/contact-emails" + 'is empty' | "/bookstore/container-without-leaves" + } + + def getDeltaReportEntities(List deltaReport) { + def xpaths = [] + def action = [] + def sourcePayload = [] + def targetPayload = [] + deltaReport.each { + delta -> xpaths.add(delta.getXpath()) + action.add(delta.getAction()) + sourcePayload.add(delta.getSourceData()) + targetPayload.add(delta.getTargetData()) + } + return ['xpaths':xpaths, 'action':action, 'sourcePayload':sourcePayload, 'targetPayload':targetPayload] + } + def countDataNodesInBookstore() { return countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', INCLUDE_ALL_DESCENDANTS)) } diff --git a/integration-test/src/test/resources/data/bookstore/bookstore.yang b/integration-test/src/test/resources/data/bookstore/bookstore.yang index 6f60f1981..9c6c42e28 100644 --- a/integration-test/src/test/resources/data/bookstore/bookstore.yang +++ b/integration-test/src/test/resources/data/bookstore/bookstore.yang @@ -49,6 +49,17 @@ module stores { } } + container support-info { + leaf support-office { + type string; + } + container contact-emails { + leaf email { + type string; + } + } + } + container container-without-leaves { } container premises { diff --git a/integration-test/src/test/resources/data/bookstore/bookstoreDataForDeltaReport.json b/integration-test/src/test/resources/data/bookstore/bookstoreDataForDeltaReport.json new file mode 100644 index 000000000..73b84fc98 --- /dev/null +++ b/integration-test/src/test/resources/data/bookstore/bookstoreDataForDeltaReport.json @@ -0,0 +1,192 @@ +{ + "bookstore-address": [ + { + "bookstore-name": "Crossword Bookstores", + "address": "Bangalore, India", + "postal-code": "560062" + } + ], + "bookstore": { + "bookstore-name": "Easons", + "premises": { + "addresses": [ + { + "house-number": 2, + "street": "Main Street", + "town": "Killarney", + "county": "Kerry" + }, + { + "house-number": 24, + "street": "Grafton Street", + "town": "Dublin", + "county": "Dublin" + } + ] + }, + "support-info": { + "contact-emails": { + } + }, + "container-without-leaves": { }, + "categories": [ + { + "code": 1, + "name": "Kids", + "books" : [ + { + "title": "Matilda", + "lang": "English", + "authors": ["Roald Dahl"], + "editions": [1988, 2000, 2023], + "price": 200 + }, + { + "title": "The Gruffalo", + "lang": "English/German", + "authors": ["Julia Donaldson"], + "editions": [1999], + "price": 15 + } + ] + }, + { + "code": 2, + "name": "Suspense" + }, + { + "code": 3, + "name": "Comedy", + "books" : [ + { + "title": "Good Omens", + "lang": "English", + "authors": ["Neil Gaiman", "Terry Pratchett"], + "editions": [2006], + "price": 13 + }, + { + "title": "The Colour of Magic", + "lang": "English", + "authors": ["Terry Pratchett"], + "editions": [1983], + "price": 12 + }, + { + "title": "The Light Fantastic", + "lang": "English", + "authors": ["Terry Pratchett"], + "editions": [1986], + "price": 14 + }, + { + "title": "A Book with No Language", + "lang": "", + "authors": ["Joe Bloggs"], + "editions": [2023], + "price": 20 + } + ] + }, + { + "code": 5, + "name": "Discount books", + "books" : [ + { + "title": "Book 1", + "lang": "blah", + "authors": [], + "editions": [], + "price": 1 + }, + { + "title": "Book 2", + "lang": "blah", + "authors": [], + "editions": [], + "price": 2 + }, + { + "title": "Book 3", + "lang": "blah", + "authors": [], + "editions": [], + "price": 3 + }, + { + "title": "Book 4", + "lang": "blah", + "authors": [], + "editions": [], + "price": 4 + }, + { + "title": "Book 5", + "lang": "blah", + "authors": [], + "editions": [], + "price": 5 + }, + { + "title": "Book 6", + "lang": "blah", + "authors": [], + "editions": [], + "price": 6 + }, + { + "title": "Book 7", + "lang": "blah", + "authors": [], + "editions": [], + "price": 7 + }, + { + "title": "Book 8", + "lang": "blahh", + "authors": [], + "editions": [], + "price": 8 + }, + { + "title": "Book 9", + "lang": "blah", + "authors": [], + "editions": [], + "price": 9 + }, + { + "title": "Book 10", + "lang": "blah", + "authors": [], + "editions": [], + "price": 10 + }, + { + "title": "Book 11", + "lang": "blah", + "authors": [], + "editions": [], + "price": 10 + } + ] + }, + { + "code": 6, + "name": "Random books", + "books": [ + { + "title": "Book 1", + "lang": "blah", + "authors": [], + "editions": [], + "price": 1 + } + ] + }, + { + "code": 7 + } + ] + } +} -- cgit 1.2.3-korg