From 206327fc6c91fccf46a2160e61e4e07ea82e6b24 Mon Sep 17 00:00:00 2001 From: Arpit Singh Date: Mon, 25 Aug 2025 19:56:02 +0530 Subject: [PATCH] Remove CpsDataService dependency from DeltaReportExecutor CpsDataService is injected as bean in DeltaReportExecutor, This is causing a cyclic dependency impacting CPS-2523(in progress). The cyclic dependency forming is as follows: CpsDataServiceImpl->CpsDeltaServiceImpl->DeltaReportExecutor->CpsDataService The solution implemented is for DeltaReportExecutor to directly call CpsDataPersistenceService Since the cps delta service does not depend on CpsDataService, the data validation steps which were present in CpsDataService have been implemented in CpsDeltaServiceImpl and DeltaReportExecutor. Issue-ID: CPS-2964 Change-Id: I1eecee22f67139eda111948d8aafad62c17340e2 Signed-off-by: Arpit Singh --- .../org/onap/cps/impl/CpsDeltaServiceImpl.java | 3 + .../cps/utils/deltareport/DeltaReportExecutor.java | 30 ++++-- .../onap/cps/impl/CpsDeltaServiceImplSpec.groovy | 4 +- .../deltareport/DeltaReportExecutorSpec.groovy | 113 +++++++++++++++------ 4 files changed, 111 insertions(+), 39 deletions(-) diff --git a/cps-service/src/main/java/org/onap/cps/impl/CpsDeltaServiceImpl.java b/cps-service/src/main/java/org/onap/cps/impl/CpsDeltaServiceImpl.java index 9d3d38b943..e9f5137464 100644 --- a/cps-service/src/main/java/org/onap/cps/impl/CpsDeltaServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/impl/CpsDeltaServiceImpl.java @@ -38,6 +38,7 @@ import org.onap.cps.api.model.Anchor; import org.onap.cps.api.model.DataNode; import org.onap.cps.api.model.DeltaReport; import org.onap.cps.api.parameters.FetchDescendantsOption; +import org.onap.cps.utils.CpsValidator; import org.onap.cps.utils.DataMapper; import org.onap.cps.utils.JsonObjectMapper; import org.onap.cps.utils.deltareport.DeltaReportExecutor; @@ -52,6 +53,7 @@ public class CpsDeltaServiceImpl implements CpsDeltaService { private final DeltaReportExecutor deltaReportExecutor; private final CpsAnchorService cpsAnchorService; + private final CpsValidator cpsValidator; private final CpsDataService cpsDataService; private final DataNodeFactory dataNodeFactory; private final DataMapper dataMapper; @@ -107,6 +109,7 @@ public class CpsDeltaServiceImpl implements CpsDeltaService { @Override public void applyChangesInDeltaReport(final String dataspaceName, final String anchorName, final String deltaReportAsJsonString) { + cpsValidator.validateNameCharacters(dataspaceName, anchorName); deltaReportExecutor.applyChangesInDeltaReport(dataspaceName, anchorName, deltaReportAsJsonString); } diff --git a/cps-service/src/main/java/org/onap/cps/utils/deltareport/DeltaReportExecutor.java b/cps-service/src/main/java/org/onap/cps/utils/deltareport/DeltaReportExecutor.java index d066a82bad..8e9a8205ab 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/deltareport/DeltaReportExecutor.java +++ b/cps-service/src/main/java/org/onap/cps/utils/deltareport/DeltaReportExecutor.java @@ -20,6 +20,7 @@ package org.onap.cps.utils.deltareport; +import static org.onap.cps.cpspath.parser.CpsPathUtil.ROOT_NODE_XPATH; import static org.onap.cps.utils.ContentType.JSON; import java.time.OffsetDateTime; @@ -28,12 +29,12 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.onap.cps.api.CpsAnchorService; -import org.onap.cps.api.CpsDataService; import org.onap.cps.api.DataNodeFactory; import org.onap.cps.api.model.Anchor; import org.onap.cps.api.model.DataNode; import org.onap.cps.api.model.DeltaReport; import org.onap.cps.cpspath.parser.CpsPathUtil; +import org.onap.cps.spi.CpsDataPersistenceService; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -46,7 +47,7 @@ public class DeltaReportExecutor { private static final OffsetDateTime NO_TIMESTAMP = null; private final CpsAnchorService cpsAnchorService; - private final CpsDataService cpsDataService; + private final CpsDataPersistenceService cpsDataPersistenceService; private final DataNodeFactory dataNodeFactory; private final JsonObjectMapper jsonObjectMapper; @@ -80,26 +81,37 @@ public class DeltaReportExecutor { private void updateDataNodes(final String dataspaceName, final String anchorName, final String xpath, final String updatedData) { - cpsDataService.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName, - CpsPathUtil.getNormalizedParentXpath(xpath), updatedData, NO_TIMESTAMP); + final String parentNodeXpath = CpsPathUtil.getNormalizedParentXpath(xpath); + final Collection dataNodesToUpdate = + createDataNodes(dataspaceName, anchorName, parentNodeXpath, updatedData); + cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodesToUpdate); } private void deleteDataNodesUsingDelta(final String dataspaceName, final String anchorName, final String xpath, final String dataToDelete) { - final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - final Collection dataNodesToDelete = - dataNodeFactory.createDataNodesWithAnchorParentXpathAndNodeData(anchor, xpath, dataToDelete, JSON); + final Collection dataNodesToDelete = createDataNodes(dataspaceName, anchorName, xpath, dataToDelete); final Collection xpathsToDelete = dataNodesToDelete.stream().map(DataNode::getXpath).toList(); - cpsDataService.deleteDataNodes(dataspaceName, anchorName, xpathsToDelete, NO_TIMESTAMP); + cpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName, xpathsToDelete); } private void addDataNodesUsingDelta(final String dataspaceName, final String anchorName, final String xpath, final String dataToAdd) { final String xpathToAdd = isRootListNodeXpath(xpath) ? CpsPathUtil.ROOT_NODE_XPATH : xpath; - cpsDataService.saveListElements(dataspaceName, anchorName, xpathToAdd, dataToAdd, NO_TIMESTAMP, JSON); + final Collection dataNodesToAdd = createDataNodes(dataspaceName, anchorName, xpathToAdd, dataToAdd); + if (ROOT_NODE_XPATH.equals(xpathToAdd)) { + cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, dataNodesToAdd); + } else { + cpsDataPersistenceService.addListElements(dataspaceName, anchorName, xpathToAdd, dataNodesToAdd); + } } private boolean isRootListNodeXpath(final String xpath) { return CpsPathUtil.getNormalizedParentXpath(xpath).isEmpty() && CpsPathUtil.isPathToListElement(xpath); } + + private Collection createDataNodes(final String datasapceName, final String anchorName, + final String xpath, final String nodeData) { + final Anchor anchor = cpsAnchorService.getAnchor(datasapceName, anchorName); + return dataNodeFactory.createDataNodesWithAnchorParentXpathAndNodeData(anchor, xpath, nodeData, JSON); + } } diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/CpsDeltaServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/CpsDeltaServiceImplSpec.groovy index 7aaeedb5ab..9f5bd4ba9c 100644 --- a/cps-service/src/test/groovy/org/onap/cps/impl/CpsDeltaServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/CpsDeltaServiceImplSpec.groovy @@ -31,6 +31,7 @@ import org.onap.cps.api.exceptions.DataValidationException import org.onap.cps.api.model.Anchor import org.onap.cps.api.model.DataNode import org.onap.cps.utils.ContentType +import org.onap.cps.utils.CpsValidator import org.onap.cps.utils.DataMapper import org.onap.cps.utils.JsonObjectMapper import org.onap.cps.utils.PrefixResolver @@ -55,6 +56,7 @@ class CpsDeltaServiceImplSpec extends Specification { def mockCpsAnchorService = Mock(CpsAnchorService) def mockCpsDataService = Mock(CpsDataService) + def mockCpsValidator = Mock(CpsValidator) def mockDeltaReportExecutor = Mock(DeltaReportExecutor) def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache) def mockTimedYangTextSchemaSourceSetBuilder = Mock(TimedYangTextSchemaSourceSetBuilder) @@ -66,7 +68,7 @@ class CpsDeltaServiceImplSpec extends Specification { def deltaReportHelper = new DeltaReportHelper() def deltaReportGenerator = new DeltaReportGenerator(deltaReportHelper) def groupedDeltaReportGenerator = new GroupedDeltaReportGenerator(deltaReportHelper) - def objectUnderTest = new CpsDeltaServiceImpl(mockDeltaReportExecutor, mockCpsAnchorService, mockCpsDataService, dataNodeFactory, dataMapper, jsonObjectMapper, deltaReportGenerator, groupedDeltaReportGenerator) + def objectUnderTest = new CpsDeltaServiceImpl(mockDeltaReportExecutor, mockCpsAnchorService, mockCpsValidator, mockCpsDataService, dataNodeFactory, dataMapper, jsonObjectMapper, deltaReportGenerator, groupedDeltaReportGenerator) static def bookstoreDataNodeWithParentXpath = [new DataNode(xpath: '/bookstore', leaves: ['bookstore-name': 'Easons'])] static def bookstoreDataNodeWithChildXpath = [new DataNode(xpath: '/bookstore/categories[@code=\'02\']', leaves: ['code': '02', 'name': 'Kids'])] diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/deltareport/DeltaReportExecutorSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/deltareport/DeltaReportExecutorSpec.groovy index 7c27efc64c..867cb2fbcd 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/deltareport/DeltaReportExecutorSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/deltareport/DeltaReportExecutorSpec.groovy @@ -19,11 +19,9 @@ package org.onap.cps.utils.deltareport - import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.TestUtils import org.onap.cps.api.CpsAnchorService -import org.onap.cps.api.CpsDataService import org.onap.cps.api.exceptions.DataValidationException import org.onap.cps.api.model.Anchor import org.onap.cps.api.model.DataNode @@ -32,6 +30,7 @@ import org.onap.cps.cpspath.parser.CpsPathUtil import org.onap.cps.impl.DataNodeFactoryImpl import org.onap.cps.impl.DeltaReportBuilder import org.onap.cps.impl.YangTextSchemaSourceSetCache +import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.utils.ContentType import org.onap.cps.utils.JsonObjectMapper import org.onap.cps.utils.YangParser @@ -45,18 +44,17 @@ import spock.lang.Specification class DeltaReportExecutorSpec extends Specification { def mockCpsAnchorService = Mock(CpsAnchorService) - def mockCpsDataService = Mock(CpsDataService) + def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService) def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache) def mockTimedYangTextSchemaSourceSetBuilder = Mock(TimedYangTextSchemaSourceSetBuilder) def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, mockTimedYangTextSchemaSourceSetBuilder) def dataNodeFactory = new DataNodeFactoryImpl(yangParser) def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) - def objectUnderTest = new DeltaReportExecutor(mockCpsAnchorService, mockCpsDataService, dataNodeFactory, jsonObjectMapper) + def objectUnderTest = new DeltaReportExecutor(mockCpsAnchorService, mockCpsDataPersistenceService, dataNodeFactory, jsonObjectMapper) @Shared static def ANCHOR_NAME_1 = 'some-anchor-1' static def ANCHOR_NAME_2 = 'some-anchor-2' - static def NO_TIMESTAMP = null def dataspaceName = 'some-dataspace' def schemaSetName = 'some-schema-set' def anchor1 = Anchor.builder().name(ANCHOR_NAME_1).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build() @@ -68,14 +66,14 @@ class DeltaReportExecutorSpec extends Specification { } def 'Perform delete operation on existing data under an anchor using delta report'() { - given: 'schema mocks and delta report in JSON format' + given: 'schema mocks and delta report as JSON' setupSchemaSetMocks('bookstore.yang') def deltaReportJson = '[{"action":"remove","xpath":"/bookstore","sourceData":{"categories":[{"code":"1","name":"Children","books":[{"title":"Matilda"}]}]}}]' and: 'delta report constructed from JSON' def deltaReport = new DeltaReportBuilder().actionRemove().withXpath('/bookstore').withSourceData('categories': [['code': '1', 'name': 'Children', 'books': [['title': 'Matilda']]]]).build() and: 'source data as JSON string from delta report' def sourceData = jsonObjectMapper.asJsonString(deltaReport.getSourceData()) - and: 'expected data nodes with child nodes to delete' + and: 'expected data nodes to delete' def dataNodes = [new DataNode(xpath: '/bookstore/categories[@code=\'1\']', childDataNodes: [new DataNode(xpath: '/bookstore/categories[@code=\'1\']/books[@title=\'Matilda\']')])] def xpathsToDelete = dataNodes*.xpath when: 'attempt to apply delta using the delta report' @@ -85,51 +83,91 @@ class DeltaReportExecutorSpec extends Specification { and: 'data nodes are built from the source data of delta report' dataNodeFactory.createDataNodesWithAnchorParentXpathAndNodeData(anchor1, deltaReport.getXpath(), sourceData, ContentType.JSON) >> [dataNodes] and: 'appropriate cps data service method is invoked with expected parameters to delete data nodes' - 1 * mockCpsDataService.deleteDataNodes(dataspaceName, ANCHOR_NAME_1, xpathsToDelete, NO_TIMESTAMP) + 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, ANCHOR_NAME_1, xpathsToDelete) } def 'Perform create operation on existing data under an anchor using delta report to add a node with #scenario'() { - given: 'schema mocks and delta report in JSON format' + given: 'schema mocks and test data' setupSchemaSetMocks('bookstore.yang') + def testData = setupTestData(scenario) + and: 'delta report as JSON' + def deltaReportJson = testData.deltaReportJson + and: 'delta report constructed from JSON' + def deltaReport = testData.deltaReport + and: 'data nodes to be created' + def dataNodes = testData.dataNodes and: 'target data as JSON string from delta report' def targetData = jsonObjectMapper.asJsonString(deltaReport.getTargetData()) when: 'attempt to apply delta using the delta report' objectUnderTest.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson) - then: 'the delta report in JSON format is converted to a list of DeltaReport objects' + then: 'the delta report as JSON string is converted to a list of DeltaReport objects' jsonObjectMapper.convertToJsonArray(deltaReportJson, DeltaReport) >> [deltaReport] + and: 'data nodes are built from the target data of delta report' + dataNodeFactory.createDataNodesWithAnchorParentXpathAndNodeData(anchor1, expectedXpath, targetData, ContentType.JSON) >> dataNodes and: 'appropriate cps data service method is invoked with expected parameters to create data nodes' - 1 * mockCpsDataService.saveListElements(dataspaceName, ANCHOR_NAME_1, expectedXpath, targetData, NO_TIMESTAMP, ContentType.JSON) + 1 * mockCpsDataPersistenceService.addListElements(dataspaceName, ANCHOR_NAME_1, expectedXpath, { it*.xpath == dataNodes*.xpath && it*.leaves == dataNodes*.leaves }) where: - scenario | deltaReportJson | deltaReport || expectedXpath - 'xpath' | '[{"action":"create","xpath":"/bookstore","targetData":{"categories":[{"code":"1","name":"Children"}]}}]' | new DeltaReportBuilder().actionCreate().withXpath('/bookstore').withTargetData(['categories': [['code': '1', 'name': 'Children']]]).build() || '/bookstore' - 'list node xpath' | '[{"action":"create","xpath":"/bookstore/categories[@code=\'1\']","targetData":{"books":[{"price":20,"title":"Matilda"}]}}]' | new DeltaReportBuilder().actionCreate().withXpath('/bookstore/categories[@code=\'1\']').withTargetData(['books': [['price': 20, 'title': 'Matilda']]]).build() || '/bookstore/categories[@code=\'1\']' - 'parent list node xpath' | '[{"action":"create","xpath":"/bookstore-address[@bookstore-name=\'Easons\']","targetData":{"address":{"street":"Main Street"}}}]' | new DeltaReportBuilder().actionCreate().withXpath('/bookstore-address[@bookstore-name=\'Easons\']').withTargetData(['address': ['street': 'Main Street']]).build() || '/' + scenario || expectedXpath + 'xpath' || '/bookstore' + 'list node xpath' || '/bookstore/categories[@code=\'1\']' } - def 'Perform replace operation on existing data under an anchor using delta report'() { + def 'Perform create operation on existing data under an anchor using delta report to add a parent list node'() { given: 'schema mocks and delta report in JSON format' setupSchemaSetMocks('bookstore.yang') - def deltaReportJson = '[{"action":"replace","xpath":"/bookstore/categories[@code=\'1\']","sourceData":{"books":[{"price":20,"title":"Matilda"}]},"targetData":{"books":[{"price":30,"title":"Matilda"}]}}]' + and: 'delta report as JSON' + def deltaReportJson = '[{"action":"create","xpath":"/bookstore-address[@bookstore-name=\'Easons\']","targetData":{"bookstore-address":[{"bookstore-name":"Easons"}]}}]' + and: 'delta report constructed from JSON' + def deltaReport = new DeltaReportBuilder().actionCreate().withXpath('/bookstore-address[@bookstore-name=\'Easons\']').withTargetData(['bookstore-address': [['bookstore-name': 'Easons']]]).build() + and: 'target data as JSON string from delta report' + def targetData = jsonObjectMapper.asJsonString(deltaReport.getTargetData()) + and: 'data nodes created from delta report' + def dataNodesCreatedFromDeltaReport = [new DataNode(xpath: '/bookstore-address[@bookstore-name=\'Easons\']', leaves: ['bookstore-name':'Easons'])] + when: 'attempt to apply delta using the delta report' + objectUnderTest.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson) + then: 'the delta report as JSON string is converted to a list of DeltaReport objects' + jsonObjectMapper.convertToJsonArray(deltaReportJson, DeltaReport) >> [deltaReport] + and: 'data nodes are built from the target data of delta report' + dataNodeFactory.createDataNodesWithAnchorParentXpathAndNodeData(anchor1, '/', targetData, ContentType.JSON) >> dataNodesCreatedFromDeltaReport + and: 'appropriate cps data service method is invoked with expected parameters to create data nodes' + 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, ANCHOR_NAME_1, dataNodesToBeStored -> + { dataNodesToBeStored*.xpath == dataNodesCreatedFromDeltaReport*.xpath && + dataNodesToBeStored*.leaves == dataNodesCreatedFromDeltaReport*.leaves }) + } + + def 'Perform replace operation on existing data under an anchor using delta report'() { + given: 'schema mocks' + setupSchemaSetMocks('bookstore.yang') + and: 'delta report as JSON string with parent and child data nodes' + def deltaReportJson = '[{"action":"replace","xpath":"/bookstore/categories[@code=\'1\']","sourceData":{"categories":[{"code":"1","name":"Children","books":[{"title":"Matilda","price":20}]}]},"targetData":{"categories":[{"code":"1","name":"Children","books":[{"title":"Matilda","price":30}]}]}}]' and: 'delta report constructed from JSON' - def deltaReport = new DeltaReportBuilder().actionReplace().withXpath('/bookstore/categories[@code=\'1\']').withSourceData(['books': [['price': 20, 'title': 'Matilda']]]).withTargetData(['books': [['price': 30, 'title': 'Matilda']]]).build() + def deltaReport = new DeltaReportBuilder().actionReplace().withXpath('/bookstore/categories[@code=\'1\']').withSourceData(['categories': [['code': '1', 'name': 'Children', 'books': [['title': 'Matilda', 'price': 20]]]]]).withTargetData(['categories': [['code': '1', 'name': 'Children', 'books': [['title': 'Matilda', 'price': 30]]]]]).build() and: 'the parent node xpath is fetched from delta report' def parentNodeXpath = CpsPathUtil.getNormalizedParentXpath(deltaReport.getXpath()) - and: 'target data as JSON is fetched from delta report' + and: 'target data as JSON string is fetched from delta report' def targetData = jsonObjectMapper.asJsonString(deltaReport.getTargetData()) + and: 'parent and child nodes to be updated' + def dataNodesCreatedFromDeltaReport = [new DataNode(xpath: '/bookstore/categories[@code=\'1\']', leaves: ['code': '1', 'name': 'Children'], childDataNodes: [new DataNode(xpath: '/bookstore/categories[@code=\'1\']/books[@title=\'Matilda\']', leaves: ['price': 30, 'title': 'Matilda'])])] when: 'attempt to apply delta using the delta report' objectUnderTest.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson) - then: 'the delta report in JSON format is converted to a list of DeltaReport objects' + then: 'the delta report as JSON string is converted to a list of DeltaReport objects' jsonObjectMapper.convertToJsonArray(deltaReportJson, DeltaReport) >> [deltaReport] - and: 'cps data service is invoked with expected parameters to delete data nodes by using their xpaths' - 1 * mockCpsDataService.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, ANCHOR_NAME_1, parentNodeXpath, targetData, NO_TIMESTAMP) + and: 'data nodes are built from the target data of delta report' + dataNodeFactory.createDataNodesWithAnchorParentXpathAndNodeData(anchor1, parentNodeXpath, targetData, ContentType.JSON) >> dataNodesCreatedFromDeltaReport + and: 'cps data service is invoked with expected parameters to update data nodes' + 1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, ANCHOR_NAME_1, dataNodesToBeStored -> + { dataNodesToBeStored*.xpath == dataNodesCreatedFromDeltaReport*.xpath && + dataNodesToBeStored*.leaves == dataNodesCreatedFromDeltaReport*.leaves }) } def 'Batch operation using delta report rolls back in case of a semantically invalid Delta Report'() { - given: 'schema mocks and a semantically invalid delta report in JSON format' + given: 'schema mocks and a semantically invalid delta report as JSON string (code: xxx is invalid value for key code)' setupSchemaSetMocks('bookstore.yang') - def deltaReportJson = '[{"action":"create","xpath":"/bookstore/categories[@code=\'100\']","targetData":{"categories":[{"code":"100","name":"Funny"}]}},{"action":"remove","xpath":"/bookstore/categories[@code=\'4\']","sourceData":{"categorie":[{"code":"4","name":"Computing"}]}}]' - and: 'delta report object with invalid data for remove operation' - def deltaReport = [new DeltaReportBuilder().actionCreate().withXpath('/bookstore/categories[@code=\'100\']').withTargetData(['categories': [['code': '100', 'name': 'Funny']]]).build(), new DeltaReportBuilder().actionRemove().withXpath('/bookstore/categories[@code=\'4\'').withSourceData(['categorie': ['code': '4', 'name': 'Computing']]).build()] + def deltaReportJson = '[{"action":"create","xpath":"/bookstore","targetData":{"categories":[{"code":"xxx","name":"Funny"}]}},{"action":"remove","xpath":"/bookstore","sourceData":{"categorie":[{"code":"4","name":"Computing"}]}}]' + and: 'delta report object with invalid data for remove operation (code: xxx is invalid value for key code)' + def deltaReport = [new DeltaReportBuilder().actionCreate().withXpath('/bookstore').withTargetData(['categories': [['code': 'xxx', 'name': 'Funny']]]).build(), new DeltaReportBuilder().actionRemove().withXpath('/bookstore').withSourceData(['categorie': ['code': '4', 'name': 'Computing']]).build()] + and: 'data nodes to be created' + def dataNodesToAdd = [new DataNode(xpath: '/bookstore/categories[@code=\'100\']', leaves: ['code': '100', 'name': 'Funny'])] and: 'appropriate data is fetched from delta report' def xpathForCreateOperation = deltaReport[0].xpath def targetDataForCreateOperation = jsonObjectMapper.asJsonString(deltaReport[0].targetData) @@ -138,12 +176,14 @@ class DeltaReportExecutorSpec extends Specification { when: 'attempt to apply delta using the delta report' objectUnderTest.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson) then: 'the delta report in JSON format is converted to DeltaReport objects' - jsonObjectMapper.convertToJsonArray(deltaReportJson, DeltaReport) >> [deltaReport] + jsonObjectMapper.convertToJsonArray(deltaReportJson, DeltaReport) >> deltaReport + and: 'data nodes are built from the target data of create operation in delta report' + dataNodeFactory.createDataNodesWithAnchorParentXpathAndNodeData(anchor1, xpathForCreateOperation, targetDataForCreateOperation, ContentType.JSON) >> dataNodesToAdd and: 'the create operation is attempted and succeeds' - 1 * mockCpsDataService.saveListElements(dataspaceName, ANCHOR_NAME_1, xpathForCreateOperation, targetDataForCreateOperation, NO_TIMESTAMP, ContentType.JSON) + 1 * mockCpsDataPersistenceService.addListElements(dataspaceName, ANCHOR_NAME_1, xpathForCreateOperation, { it }) and: 'the remove operation fails due to invalid data, causing rollback' dataNodeFactory.createDataNodesWithAnchorParentXpathAndNodeData(anchor1, xpathForDeleteOperation, sourceDataForDeleteOperation, ContentType.JSON) >> {throw new DataValidationException('Data Validation Failed')} - then: 'a DataValidationException is thrown' + and: 'a DataValidationException is thrown' thrown(DataValidationException) } @@ -154,4 +194,19 @@ class DeltaReportExecutorSpec extends Specification { def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext } + + def setupTestData(scenario) { + if (scenario == 'xpath') { + return [ + deltaReportJson: '[{"action":"create","xpath":"/bookstore","targetData":{"categories":[{"code":"1","name":"Children"}]}}]', + deltaReport: new DeltaReportBuilder().actionCreate().withXpath('/bookstore').withTargetData(['categories': [['code': '1', 'name': 'Children']]]).build(), + dataNodes: [new DataNode(xpath: '/bookstore/categories[@code=\'1\']', leaves: ['code': '1', 'name': 'Children'])] + ] + } + return [ + deltaReportJson: '[{"action":"create","xpath":"/bookstore/categories[@code=\'1\']","targetData":{"books":[{"price":20,"title":"Matilda"}]}}]', + deltaReport: new DeltaReportBuilder().actionCreate().withXpath('/bookstore/categories[@code=\'1\']').withTargetData(['books': [['price': 20, 'title': 'Matilda']]]).build(), + dataNodes: [new DataNode(xpath: '/bookstore/categories[@code=\'1\']/books[@title=\'Matilda\']', leaves: ['price':20, 'title':'Matilda'])] + ] + } } -- 2.16.6