Merge "Apostrophe handling in CpsPathParser"
authorToine Siebelink <toine.siebelink@est.tech>
Thu, 20 Jul 2023 13:27:18 +0000 (13:27 +0000)
committerGerrit Code Review <gerrit@onap.org>
Thu, 20 Jul 2023 13:27:18 +0000 (13:27 +0000)
cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/resources/bookstore.json
cps-service/src/test/resources/bookstore.yang
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
integration-test/src/test/resources/data/bookstore/bookstore.yang
integration-test/src/test/resources/data/bookstore/bookstoreData.json

index d88a9cd..81262c8 100755 (executable)
@@ -179,6 +179,29 @@ class DataRestControllerSpec extends Specification {
             'without observed-timestamp XML'  | null                           | MediaType.APPLICATION_XML  | requestBodyXml  | expectedXmlData  | ContentType.XML
     }
 
+    def 'save list elements under root node #scenario.'() {
+        given: 'root node xpath '
+            def rootNodeXpath = '/'
+        when: 'list-node endpoint is invoked with post (create) operation'
+            def postRequestBuilder = post("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
+                .contentType(MediaType.APPLICATION_JSON)
+                .param('xpath', rootNodeXpath )
+                .content(requestBodyJson)
+            if (observedTimestamp != null)
+                postRequestBuilder.param('observed-timestamp', observedTimestamp)
+            def response = mvc.perform(postRequestBuilder).andReturn().response
+        then: 'a created response is returned'
+            response.status == expectedHttpStatus.value()
+        then: 'the java API was called with the correct parameters'
+            expectedApiCount * mockCpsDataService.saveListElements(dataspaceName, anchorName, rootNodeXpath, expectedJsonData,
+                { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+        where:
+            scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
+            'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.CREATED
+            'without observed-timestamp'      | null                           || 1                | HttpStatus.CREATED
+            'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
+    }
+
     def 'Save list elements #scenario.'() {
         given: 'parent node xpath '
             def parentNodeXpath = 'parent node xpath'
index 6e7c164..7db87e8 100755 (executable)
@@ -116,8 +116,12 @@ public class CpsDataServiceImpl implements CpsDataService {
         final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
         final Collection<DataNode> listElementDataNodeCollection =
             buildDataNodes(anchor, parentNodeXpath, jsonData, ContentType.JSON);
-        cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath,
-            listElementDataNodeCollection);
+        if (isRootNodeXpath(parentNodeXpath)) {
+            cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, listElementDataNodeCollection);
+        } else {
+            cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath,
+                                                      listElementDataNodeCollection);
+        }
         processDataUpdatedEventAsync(anchor, parentNodeXpath, UPDATE, observedTimestamp);
     }
 
@@ -391,6 +395,10 @@ public class CpsDataServiceImpl implements CpsDataService {
             .get(anchor.getDataspaceName(), anchor.getSchemaSetName()).getSchemaContext();
     }
 
+    private static boolean isRootNodeXpath(final String xpath) {
+        return ROOT_NODE_XPATH.equals(xpath);
+    }
+
     private void processDataNodeUpdate(final Anchor anchor, final DataNode dataNodeUpdate) {
         cpsDataPersistenceService.batchUpdateDataLeaves(anchor.getDataspaceName(), anchor.getName(),
                 Collections.singletonMap(dataNodeUpdate.getXpath(), dataNodeUpdate.getLeaves()));
index db86640..ba43849 100644 (file)
@@ -110,6 +110,28 @@ class CpsDataServiceImplSpec extends Specification {
             noExceptionThrown()
     }
 
+    def 'Saving list element data fragment under Root node.'() {
+        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}]}'
+            objectUnderTest.saveListElements(dataspaceName, anchorName, '/', jsonData, observedTimestamp)
+        then: 'the persistence service method is invoked with correct parameters'
+            1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
+                { dataNodeCollection ->
+                    {
+                        assert dataNodeCollection.size() == 1
+                        assert dataNodeCollection.collect { it.getXpath() }
+                            .containsAll(['/invoice[@ProductID=\'2\']'])
+                    }
+                }
+            )
+        and: 'the CpsValidator is called on the dataspaceName and AnchorName'
+            1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
+        and: 'data updated event is sent to notification service'
+            1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.UPDATE, observedTimestamp)
+    }
+
     def 'Saving child data fragment under existing node.'() {
         given: 'schema set for given anchor and dataspace references test-tree model'
             setupSchemaSetMocks('test-tree.yang')
index 459908b..4b8ed3d 100644 (file)
@@ -1,4 +1,12 @@
 {
+   "multiple-data-tree:invoice": [
+      {
+         "ProductID": "1",
+         "ProductName": "Apple",
+         "price": "100",
+         "stock": false
+      }
+   ],
    "test:bookstore":{
       "bookstore-name": "Chapters/Easons",
       "categories": [
index 2179fb9..b7a52e2 100644 (file)
@@ -15,6 +15,34 @@ module stores {
         }
     }
 
+    list invoice {
+        key "ProductID";
+        leaf ProductID {
+            type uint64;
+            mandatory "true";
+            description
+            "Unique product ID. Example: 001";
+        }
+        leaf ProductName {
+            type string;
+            mandatory "true";
+            description
+            "Name of the Product";
+        }
+        leaf price {
+            type uint64;
+            mandatory "true";
+            description
+            "Price of book";
+        }
+        leaf stock {
+            type boolean;
+            default "false";
+            description
+            "Book in stock or not. Example value: true";
+        }
+    }
+
     container bookstore {
 
         leaf bookstore-name {
index 750deb1..a3f1439 100644 (file)
@@ -43,11 +43,13 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
 
     CpsDataService objectUnderTest
     def originalCountBookstoreChildNodes
+    def originalCountBookstoreTopLevelListNodes
     def now = OffsetDateTime.now()
 
     def setup() {
         objectUnderTest = cpsDataService
         originalCountBookstoreChildNodes = countDataNodesInBookstore()
+        originalCountBookstoreTopLevelListNodes = countTopLevelListDataNodesInBookstore()
     }
 
     def 'Read bookstore top-level container(s) using #fetchDescendantsOption.'() {
@@ -73,9 +75,9 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
         when: 'get data nodes for bookstore container'
             def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, root, OMIT_DESCENDANTS)
         then: 'the tree consist ouf of one data node'
-            assert countDataNodesInTree(result) == 1
+            assert countDataNodesInTree(result) == 2
         and: 'the top level data node has the expected attribute and value'
-            assert result.leaves['bookstore-name'] == ['Easons']
+            assert result.leaves.size() == 2
         where: 'the following variations of "root" are used'
             root << [ '/', '' ]
     }
@@ -179,6 +181,21 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
             thrown(DataNodeNotFoundExceptionBatch)
     }
 
+    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}]}'
+        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)
+        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)
+        then: 'the original number of datanodes is restored'
+            assert originalCountBookstoreTopLevelListNodes == countTopLevelListDataNodesInBookstore()
+    }
+
     def 'Add and Delete list (element) data nodes.'() {
         given: 'two new (categories) data nodes'
             def json = '{"categories": [ {"code":"new1"}, {"code":"new2" } ] }'
@@ -368,4 +385,8 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
     def countDataNodesInBookstore() {
         return countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', INCLUDE_ALL_DESCENDANTS))
     }
+
+    def countTopLevelListDataNodesInBookstore() {
+        return countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/', INCLUDE_ALL_DESCENDANTS))
+    }
 }
index e592a9c..ab384de 100644 (file)
@@ -15,6 +15,34 @@ module stores {
         }
     }
 
+    list invoice {
+        key "ProductID";
+        leaf ProductID {
+            type uint64;
+            mandatory "true";
+            description
+            "Unique product ID. Example: 001";
+        }
+        leaf ProductName {
+            type string;
+            mandatory "true";
+            description
+            "Name of the Product";
+        }
+        leaf price {
+            type uint64;
+            mandatory "true";
+            description
+            "Price of book";
+        }
+        leaf stock {
+            type boolean;
+            default "false";
+            description
+            "Book in stock or not. Example value: true";
+        }
+    }
+
     container bookstore {
 
         leaf bookstore-name {
index 5d22f6d..5f66a1d 100644 (file)
@@ -1,4 +1,12 @@
 {
+  "multiple-data-tree:invoice": [
+    {
+      "ProductID": "1",
+      "ProductName": "Apple",
+      "price": "100",
+      "stock": false
+    }
+  ],
   "bookstore": {
     "bookstore-name": "Easons",
     "premises": {