Merge "Fix: Make bookstore data consistent"
authorToine Siebelink <toine.siebelink@est.tech>
Tue, 1 Aug 2023 12:20:51 +0000 (12:20 +0000)
committerGerrit Code Review <gerrit@onap.org>
Tue, 1 Aug 2023 12:20:51 +0000 (12:20 +0000)
1  2 
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy

@@@ -29,11 -29,7 +29,11 @@@ import org.onap.cps.notification.Notifi
  import org.onap.cps.notification.Operation
  import org.onap.cps.spi.CpsDataPersistenceService
  import org.onap.cps.spi.FetchDescendantsOption
 +import org.onap.cps.spi.exceptions.ConcurrencyException
 +import org.onap.cps.spi.exceptions.DataNodeNotFoundExceptionBatch
  import org.onap.cps.spi.exceptions.DataValidationException
 +import org.onap.cps.spi.exceptions.SessionManagerException
 +import org.onap.cps.spi.exceptions.SessionTimeoutException
  import org.onap.cps.spi.model.Anchor
  import org.onap.cps.spi.model.DataNode
  import org.onap.cps.spi.model.DataNodeBuilder
@@@ -118,7 -114,7 +118,7 @@@ class CpsDataServiceImplSpec extends Sp
          given: 'schema set for given anchor and dataspace references bookstore model'
              setupSchemaSetMocks('bookstore.yang')
          when: 'save data method is invoked with list element json data'
-             def jsonData = '{"multiple-data-tree:invoice": [{"ProductID": "2","ProductName": "Banana","price": "100","stock": True}]}'
+             def jsonData = '{"bookstore-address":[{"bookstore-name":"Easons","address":"Dublin,Ireland","postal-code":"D02HA21"}]}'
              objectUnderTest.saveListElements(dataspaceName, anchorName, '/', jsonData, observedTimestamp)
          then: 'the persistence service method is invoked with correct parameters'
              1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
                      {
                          assert dataNodeCollection.size() == 1
                          assert dataNodeCollection.collect { it.getXpath() }
-                             .containsAll(['/invoice[@ProductID=\'2\']'])
+                             .containsAll(['/bookstore-address[@bookstore-name=\'Easons\']'])
                      }
                  }
              )
              'level 2 node'   | ['/test-tree' : '{"branch": [{"name":"Name"}]}', '/test-tree/branch[@name=\'Name\']':'{"nest":{"name":"nestName"}}'] || ["/test-tree/branch[@name='Name']", "/test-tree/branch[@name='Name']/nest"]
      }
  
 +    def 'Replace data node with concurrency exception in persistence layer.'() {
 +        given: 'the persistence layer throws an concurrency exception'
 +            def originalException = new ConcurrencyException('message', 'details')
 +            mockCpsDataPersistenceService.updateDataNodesAndDescendants(*_) >> { throw originalException }
 +            setupSchemaSetMocks('test-tree.yang')
 +        when: 'attempt to replace data node'
 +            objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, ['/' : '{"test-tree": {}}'] , observedTimestamp)
 +        then: 'the same exception is thrown up'
 +            def thrownUp = thrown(ConcurrencyException)
 +            assert thrownUp == originalException
 +    }
 +
      def 'Replace list content data fragment under parent node.'() {
          given: 'schema set for given anchor and dataspace references test-tree model'
              setupSchemaSetMocks('test-tree.yang')
      }
  
      def 'Delete list element under existing node.'() {
 -        given: 'schema set for given anchor and dataspace references test-tree model'
 -            setupSchemaSetMocks('test-tree.yang')
          when: 'delete list data method is invoked with list element json data'
              objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp)
          then: 'the persistence service method is invoked with correct parameters'
      }
  
      def 'Delete multiple list elements under existing node.'() {
 -        given: 'schema set for given anchor and dataspace references test-tree model'
 -            setupSchemaSetMocks('test-tree.yang')
          when: 'delete multiple list data method is invoked with list element json data'
              objectUnderTest.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'], observedTimestamp)
          then: 'the persistence service method is invoked with correct parameters'
      }
  
      def 'Delete data node under anchor and dataspace.'() {
 -        given: 'schema set for given anchor and dataspace references test tree model'
 -            setupSchemaSetMocks('test-tree.yang')
          when: 'delete data node method is invoked with correct parameters'
              objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
          then: 'the persistence service method is invoked with the correct parameters'
      }
  
      def 'Delete all data nodes for a given anchor and dataspace.'() {
 -        given: 'schema set for given anchor and dataspace references test tree model'
 -            setupSchemaSetMocks('test-tree.yang')
 -        when: 'delete data node method is invoked with correct parameters'
 +        when: 'delete data nodes method is invoked with correct parameters'
              objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
          then: 'data updated event is sent to notification service before the delete'
              1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.DELETE, observedTimestamp)
              1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName)
      }
  
 +    def 'Delete all data nodes for a given anchor and dataspace with batch exception in persistence layer.'() {
 +        given: 'a batch exception in persistence layer'
 +            def originalException = new DataNodeNotFoundExceptionBatch('ds1','a1',[])
 +            mockCpsDataPersistenceService.deleteDataNodes(*_)  >> { throw originalException }
 +        when: 'attempt to delete data nodes'
 +            objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
 +        then: 'the original exception is thrown up'
 +            def thrownUp = thrown(DataNodeNotFoundExceptionBatch)
 +            assert thrownUp == originalException
 +        and: 'the exception details contain the expected data'
 +            assert thrownUp.details.contains('ds1')
 +            assert thrownUp.details.contains('a1')
 +    }
 +
      def 'Delete all data nodes for given dataspace and multiple anchors.'() {
          given: 'schema set for given anchors and dataspace references test tree model'
              setupSchemaSetMocks('test-tree.yang')
              1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, _ as Collection<String>)
      }
  
 -    def setupSchemaSetMocks(String... yangResources) {
 -        def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
 -        mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
 -        def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
 -        def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
 -        mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
 -    }
 -
 -    def 'start session'() {
 +    def 'Start session.'() {
          when: 'start session method is called'
              objectUnderTest.startSession()
          then: 'the persistence service method to start session is invoked'
              1 * mockCpsDataPersistenceService.startSession()
      }
  
 -    def 'close session'(){
 +    def 'Start session with Session Manager Exceptions.'() {
 +        given: 'the persistence layer throws an Session Manager Exception'
 +            mockCpsDataPersistenceService.startSession() >> { throw originalException }
 +        when: 'attempt to start session'
 +            objectUnderTest.startSession()
 +        then: 'the original exception is thrown up'
 +            def thrownUp = thrown(SessionManagerException)
 +            assert thrownUp == originalException
 +        where: 'variations of Session Manager Exception are used'
 +            originalException << [ new SessionManagerException('message','details'),
 +                                   new SessionManagerException('message','details', new Exception('cause')),
 +                                   new SessionTimeoutException('message','details', new Exception('cause'))]
 +    }
 +
 +    def 'Close session.'(){
          given: 'session Id from calling the start session method'
              def sessionId = objectUnderTest.startSession()
          when: 'close session method is called'
              1 * mockCpsDataPersistenceService.closeSession(sessionId)
      }
  
 -    def 'lock anchor with no timeout parameter'(){
 +    def 'Lock anchor with no timeout parameter.'(){
          when: 'lock anchor method with no timeout parameter with details of anchor entity to lock'
              objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName')
          then: 'the persistence service method to lock anchor is invoked with default timeout'
 -            1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName',
 -                    'some-anchorName', 300L)
 +            1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 300L)
      }
  
 -    def 'lock anchor with timeout parameter'(){
 +    def 'Lock anchor with timeout parameter.'(){
          when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock'
 -            objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName',
 -                    'some-anchorName', 250L)
 +            objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
          then: 'the persistence service method to lock anchor is invoked with the given timeout'
 -            1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName',
 -                    'some-anchorName', 250L)
 +            1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
 +    }
 +
 +    def setupSchemaSetMocks(String... yangResources) {
 +        def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
 +        mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
 +        def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
 +        def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
 +        mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
      }
 +
  }
@@@ -113,49 -113,23 +113,49 @@@ class CpsDataServiceIntegrationSpec ext
              restoreBookstoreDataAnchor(1)
      }
  
 +    def 'Get whole list data' () {
 +            def xpathForWholeList = "/bookstore/categories"
 +        when: 'get data nodes for bookstore container'
 +            def dataNodes = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, xpathForWholeList, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
 +        then: 'the tree consist ouf of #expectNumberOfDataNodes data nodes'
 +            assert dataNodes.size() == 5
 +        and: 'each datanode contains the list node xpath partially in its xpath'
 +            dataNodes.each {dataNode ->
 +                assert dataNode.xpath.contains(xpathForWholeList)
 +            }
 +    }
 +
 +    def 'Read (multiple) data nodes with #scenario' () {
 +        when: 'attempt to get data nodes using multiple valid xpaths'
 +            def dataNodes = objectUnderTest.getDataNodesForMultipleXpaths(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, xpath, OMIT_DESCENDANTS)
 +        then: 'expected numer of data nodes are returned'
 +            dataNodes.size() == expectedNumberOfDataNodes
 +        where: 'the following data was used'
 +                    scenario                    |                       xpath                                       |   expectedNumberOfDataNodes
 +            'container-node xpath'              | ['/bookstore']                                                    |               1
 +            'list-item'                         | ['/bookstore/categories[@code=1]']                                |               1
 +            'parent-list xpath'                 | ['/bookstore/categories']                                         |               5
 +            'child-list xpath'                  | ['/bookstore/categories[@code=1]/books']                          |               2
 +            'both parent and child list xpath'  | ['/bookstore/categories', '/bookstore/categories[@code=1]/books'] |               7
 +    }
 +
      def 'Add and Delete a (container) data node using #scenario.'() {
 -        when: 'the new datanode is saved'
 -            objectUnderTest.saveData(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , parentXpath, json, now)
 -        then: 'it can be retrieved by its normalized xpath'
 -            def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, normalizedXpathToNode, DIRECT_CHILDREN_ONLY)
 -            assert result.size() == 1
 -            assert result[0].xpath == normalizedXpathToNode
 -        and: 'there is now one extra datanode'
 -            assert originalCountBookstoreChildNodes + 1 == countDataNodesInBookstore()
 -        when: 'the new datanode is deleted'
 -            objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, normalizedXpathToNode, now)
 -        then: 'the original number of data nodes is restored'
 -            assert originalCountBookstoreChildNodes == countDataNodesInBookstore()
 -        where:
 -            scenario                      | parentXpath                         | json                                                                                        || normalizedXpathToNode
 -            'normalized parent xpath'     | '/bookstore'                        | '{"webinfo": {"domain-name":"ourbookstore.com", "contact-email":"info@ourbookstore.com" }}' || "/bookstore/webinfo"
 -            'non-normalized parent xpath' | '/bookstore/categories[ @code="1"]' | '{"books": {"title":"new" }}'                                                               || "/bookstore/categories[@code='1']/books[@title='new']"
 +            when: 'the new datanode is saved'
 +                objectUnderTest.saveData(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , parentXpath, json, now)
 +            then: 'it can be retrieved by its normalized xpath'
 +                def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, normalizedXpathToNode, DIRECT_CHILDREN_ONLY)
 +                assert result.size() == 1
 +                assert result[0].xpath == normalizedXpathToNode
 +            and: 'there is now one extra datanode'
 +                assert originalCountBookstoreChildNodes + 1 == countDataNodesInBookstore()
 +            when: 'the new datanode is deleted'
 +                objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, normalizedXpathToNode, now)
 +            then: 'the original number of data nodes is restored'
 +                assert originalCountBookstoreChildNodes == countDataNodesInBookstore()
 +            where:
 +                scenario                      | parentXpath                         | json                                                                                        || normalizedXpathToNode
 +                'normalized parent xpath'     | '/bookstore'                        | '{"webinfo": {"domain-name":"ourbookstore.com", "contact-email":"info@ourbookstore.com" }}' || "/bookstore/webinfo"
 +                'non-normalized parent xpath' | '/bookstore/categories[ @code="1"]' | '{"books": {"title":"new" }}'                                                               || "/bookstore/categories[@code='1']/books[@title='new']"
      }
  
      def 'Attempt to create a top level data node using root.'() {
  
      def 'Add and Delete top-level list (element) data nodes with root node.'() {
          given: 'a new (multiple-data-tree:invoice) datanodes'
-             def json = '{"multiple-data-tree:invoice": [{"ProductID": "2","ProductName": "Mango","price": "150","stock": true}]}'
+             def json = '{"bookstore-address":[{"bookstore-name":"Scholastic","address":"Bangalore,India","postal-code":"560043"}]}'
          when: 'the new list elements are saved'
              objectUnderTest.saveListElements(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/', json, now)
          then: 'they can be retrieved by their xpaths'
-             objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/invoice[@ProductID ="2"]', INCLUDE_ALL_DESCENDANTS)
+             objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore-address[@bookstore-name="Easons"]', INCLUDE_ALL_DESCENDANTS)
          and: 'there is one extra datanode'
              assert originalCountBookstoreTopLevelListNodes + 1 == countTopLevelListDataNodesInBookstore()
          when: 'the new elements are deleted'
-             objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/invoice[@ProductID ="2"]', now)
+             objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore-address[@bookstore-name="Easons"]', now)
          then: 'the original number of datanodes is restored'
              assert originalCountBookstoreTopLevelListNodes == countTopLevelListDataNodesInBookstore()
      }