Merge "Fix: Make bookstore data consistent"
[cps.git] / cps-service / src / test / groovy / org / onap / cps / api / impl / CpsDataServiceImplSpec.groovy
index faa5d2e..b4ac7a6 100644 (file)
@@ -29,7 +29,11 @@ import org.onap.cps.notification.NotificationService
 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
@@ -64,26 +68,6 @@ class CpsDataServiceImplSpec extends Specification {
     def anchor = Anchor.builder().name(anchorName).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build()
     def observedTimestamp = OffsetDateTime.now()
 
-    def 'Saving multicontainer json data.'() {
-        given: 'schema set for given anchor and dataspace references test-tree model'
-            setupSchemaSetMocks('multipleDataTree.yang')
-        when: 'save data method is invoked with test-tree json data'
-            def jsonData = TestUtils.getResourceFileContent('multiple-object-data.json')
-            objectUnderTest.saveData(dataspaceName, anchorName, jsonData, observedTimestamp)
-        then: 'the persistence service method is invoked with correct parameters'
-            1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
-                { dataNode -> dataNode.xpath[index] == xpath })
-        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.CREATE, observedTimestamp)
-        where:
-            index   |   xpath
-                0   | '/first-container'
-                1   | '/last-container'
-
-    }
-
     def 'Saving #scenario data.'() {
         given: 'schema set for given anchor and dataspace references test-tree model'
             setupSchemaSetMocks('test-tree.yang')
@@ -103,19 +87,54 @@ class CpsDataServiceImplSpec extends Specification {
             'xml'    | 'test-tree.xml'  | ContentType.XML
     }
 
-    def 'Saving #scenarioDesired data with invalid data.'() {
+    def 'Saving data with error: #scenario.'() {
         given: 'schema set for given anchor and dataspace references test-tree model'
-        setupSchemaSetMocks('test-tree.yang')
+            setupSchemaSetMocks('test-tree.yang')
         when: 'save data method is invoked with test-tree json data'
             objectUnderTest.saveData(dataspaceName, anchorName, invalidData, observedTimestamp, contentType)
-        then: 'a data validation exception is thrown'
-            thrown(DataValidationException)
+        then: 'a data validation exception is thrown with the correct message'
+            def exceptionThrown  = thrown(DataValidationException)
+            assert exceptionThrown.message.startsWith(expectedMessage)
         where: 'given parameters'
-            scenarioDesired | invalidData             | contentType
-            'json'          | '{invalid  json'        | ContentType.XML
-            'xml'           | '<invalid xml'          | ContentType.JSON
+            scenario        | invalidData     | contentType      || expectedMessage
+            'no data nodes' | '{}'            | ContentType.JSON || 'No data nodes'
+            'invalid json'  | '{invalid json' | ContentType.JSON || 'Failed to parse json data'
+            'invalid xml'   | '<invalid xml'  | ContentType.XML  || 'Failed to parse xml data'
+    }
+
+    def 'Saving #scenarioDesired data exception during notification.'() {
+        given: 'schema set for given anchor and dataspace references test-tree model'
+            setupSchemaSetMocks('test-tree.yang')
+        and: 'the notification service throws an exception'
+            mockNotificationService.processDataUpdatedEvent(*_) >> { throw new RuntimeException('to be ignored')}
+        when: 'save data method is invoked with test-tree json data'
+            def data = TestUtils.getResourceFileContent('test-tree.json')
+            objectUnderTest.saveData(dataspaceName, anchorName, data, observedTimestamp)
+        then: 'the exception is ignored'
+            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 = '{"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,
+                { dataNodeCollection ->
+                    {
+                        assert dataNodeCollection.size() == 1
+                        assert dataNodeCollection.collect { it.getXpath() }
+                            .containsAll(['/bookstore-address[@bookstore-name=\'Easons\']'])
+                    }
+                }
+            )
+        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'
@@ -215,15 +234,15 @@ class CpsDataServiceImplSpec extends Specification {
         when: 'update data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
             objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
         then: 'the persistence service method is invoked with correct parameters'
-            1 * mockCpsDataPersistenceService.updateDataLeaves(dataspaceName, anchorName, expectedNodeXpath, leaves)
+            1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, {dataNode -> dataNode.keySet()[0] == expectedNodeXpath})
         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, parentNodeXpath, Operation.UPDATE, observedTimestamp)
         where: 'following parameters were used'
-            scenario         | parentNodeXpath | jsonData                        || expectedNodeXpath                   | leaves
-            'top level node' | '/'             | '{"test-tree": {"branch": []}}' || '/test-tree'                        | Collections.emptyMap()
-            'level 2 node'   | '/test-tree'    | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']' | ['name': 'Name']
+            scenario         | parentNodeXpath | jsonData                        || expectedNodeXpath
+            'top level node' | '/'             | '{"test-tree": {"branch": []}}' || '/test-tree'
+            'level 2 node'   | '/test-tree'    | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']'
     }
 
     def 'Update list-element data node with : #scenario.'() {
@@ -235,22 +254,46 @@ class CpsDataServiceImplSpec extends Specification {
         then: 'the persistence service method is invoked with correct parameters'
             thrown(DataValidationException)
         where: 'following parameters were used'
-            scenario          | jsonData
+            scenario                  | jsonData
             'multiple expectedLeaves' | '{"code": "01","name": "some-name"}'
-            'one leaf'        | '{"name": "some-name"}'
+            'one leaf'                | '{"name": "some-name"}'
     }
 
-    def 'Update Bookstore node leaves' () {
+    def 'Update data nodes in different containers.' () {
+        given: 'schema set for given dataspace and anchor refers multipleDataTree model'
+            setupSchemaSetMocks('multipleDataTree.yang')
+        and: 'json string with multiple data trees'
+            def parentNodeXpath = '/'
+            def updatedJsonData = '{"first-container":{"a-leaf":"a-new-Value"},"last-container":{"x-leaf":"x-new-value"}}'
+        when: 'update operation is performed on multiple data nodes'
+            objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, updatedJsonData, observedTimestamp)
+        then: 'the persistence service method is invoked with correct parameters'
+            1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, {dataNode -> dataNode.keySet()[index] == expectedNodeXpath})
+        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, parentNodeXpath, Operation.UPDATE, observedTimestamp)
+        where: 'the following parameters were used'
+            index | expectedNodeXpath
+            0     | '/first-container'
+            1     | '/last-container'
+    }
+
+    def 'Update Bookstore node leaves and child.' () {
         given: 'a DMI registry model'
             setupSchemaSetMocks('bookstore.yang')
-        and: 'the expected json string'
-            def jsonData = '{"categories":[{"code":01,"name":"Romance"}]}'
+        and: 'json update for a category (parent) and new book (child)'
+            def jsonData = '{"categories":[{"code":01,"name":"Romance","books": [{"title": "new"}]}]}'
         when: 'update data method is invoked with json data and parent node xpath'
-            objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName,
-                '/bookstore', jsonData, observedTimestamp)
-        then: 'the persistence service method is invoked with correct parameters'
-            1 * mockCpsDataPersistenceService.updateDataLeaves(dataspaceName, anchorName,
-                "/bookstore/categories[@code='01']", ['name':'Romance', 'code': '01'])
+            objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName, '/bookstore', jsonData, observedTimestamp)
+        then: 'the persistence service method is invoked for the category (parent)'
+            1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName,
+                    {updatedDataNodesPerXPath -> updatedDataNodesPerXPath.keySet()
+                                                .iterator().next() == "/bookstore/categories[@code='01']"})
+        and: 'the persistence service method is invoked for the new book (child)'
+            1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName,
+                {updatedDataNodesPerXPath -> updatedDataNodesPerXPath.keySet()
+                    .iterator().next() == "/bookstore/categories[@code='01']/books[@title='new']"})
         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
         and: 'the data updated event is sent to the notification service'
@@ -294,6 +337,18 @@ class CpsDataServiceImplSpec extends Specification {
             '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')
@@ -327,8 +382,6 @@ class CpsDataServiceImplSpec extends Specification {
     }
 
     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'
@@ -340,8 +393,6 @@ class CpsDataServiceImplSpec extends Specification {
     }
 
     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'
@@ -353,8 +404,6 @@ class CpsDataServiceImplSpec extends Specification {
     }
 
     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'
@@ -366,9 +415,7 @@ class CpsDataServiceImplSpec extends Specification {
     }
 
     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)
@@ -378,6 +425,20 @@ class CpsDataServiceImplSpec extends Specification {
             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')
@@ -394,22 +455,28 @@ class CpsDataServiceImplSpec extends Specification {
             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'
@@ -418,20 +485,26 @@ class CpsDataServiceImplSpec extends Specification {
             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
+    }
+
 }