From 5bc03e1ccbab09cce49d8f50d067d8dab53bb2fb Mon Sep 17 00:00:00 2001 From: Arpit Singh Date: Wed, 3 Dec 2025 17:17:59 +0530 Subject: [PATCH] Fix Replace a Node API behaviour handling List items Jira - [CPS-2708] Enhance Replace A Node with List Items Documentation - https://lf-onap.atlassian.net/wiki/x/gIDMAQ - Previously, Replace a node endpoint accepted list items and returned a 200 OK response, but the data was not actually replaced when cross checked. - This patch fixes the issue of phantom replace operations and ensures that the API correctly handles list items. Issue-ID:CPS-2708 Change-Id: Ibc9bbb88ccbb07302355321c6d5c2eade0e7e5fa Signed-off-by: Arpit Singh --- cps-rest/docs/openapi/cpsData.yml | 2 ++ .../java/org/onap/cps/impl/CpsDataServiceImpl.java | 7 ++++++- .../onap/cps/impl/CpsDataServiceImplSpec.groovy | 19 ++++++++++++++++++ .../cps/DataServiceIntegrationSpec.groovy | 23 +++++++++++++++++++++- 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/cps-rest/docs/openapi/cpsData.yml b/cps-rest/docs/openapi/cpsData.yml index 178a68fb77..ad4fccb5d9 100644 --- a/cps-rest/docs/openapi/cpsData.yml +++ b/cps-rest/docs/openapi/cpsData.yml @@ -240,6 +240,8 @@ nodesByDataspaceAndAnchor: responses: '200': $ref: 'components.yml#/components/responses/Ok' + '204': + $ref: 'components.yml#/components/responses/Created' '400': $ref: 'components.yml#/components/responses/BadRequest' '403': diff --git a/cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java index a8aee62a3c..e3da6c59da 100644 --- a/cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java @@ -26,6 +26,7 @@ package org.onap.cps.impl; import static org.onap.cps.cpspath.parser.CpsPathUtil.NO_PARENT_PATH; import static org.onap.cps.cpspath.parser.CpsPathUtil.ROOT_NODE_XPATH; +import static org.onap.cps.cpspath.parser.CpsPathUtil.isPathToListElement; import static org.onap.cps.events.model.EventPayload.Action.CREATE; import static org.onap.cps.events.model.EventPayload.Action.REMOVE; import static org.onap.cps.events.model.EventPayload.Action.REPLACE; @@ -209,7 +210,11 @@ public class CpsDataServiceImpl implements CpsDataService { final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); final Collection dataNodes = dataNodeFactory .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); - cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes); + if (ROOT_NODE_XPATH.equals(parentNodeXpath) || !isPathToListElement(parentNodeXpath)) { + cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes); + } else { + cpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, parentNodeXpath, dataNodes); + } sendDataUpdatedEvent(anchor, parentNodeXpath, REPLACE_ACTION, observedTimestamp); } diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy index af2c50f9ea..1aedb74882 100644 --- a/cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy @@ -310,6 +310,25 @@ class CpsDataServiceImplSpec extends Specification { 'json list' | '/test-tree' | '{"branch": [{"name":"Name1"}, {"name":"Name2"}]}' || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"] } + def 'Replace list data node using singular #scenario node'() { + given: 'schema set for given anchor and dataspace references test-tree model' + setupSchemaSetMocks('test-tree.yang') + when: 'replace data method is invoked with data and list node xpath' + objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, '/test-tree/branch[@name=\'Name\']', data, observedTimestamp, contentType) + then: 'the persistence service method is invoked with correct parameters' + 1 * mockCpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, '/test-tree/branch[@name=\'Name\']', + { dataNodes ->{ + assert dataNodes.size() == 1 + assert dataNodes.collect { it.getXpath() == '/test-tree/branch[@name=\'Name\']/nest' } + }}) + and: 'the CpsValidator is called on the dataspaceName and AnchorName' + 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) + where: 'the following data was used' + scenario | contentType | data + 'JSON data' | ContentType.JSON | '{"nest":{"name":"nestName"}}' + 'XML data' | ContentType.XML | 'nestName' + } + def 'Replace data node using singular XML data node: #scenario.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DataServiceIntegrationSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DataServiceIntegrationSpec.groovy index 802438f8e3..0390b740ea 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DataServiceIntegrationSpec.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DataServiceIntegrationSpec.groovy @@ -431,7 +431,7 @@ class DataServiceIntegrationSpec extends FunctionalSpecBase { when: 'the webinfo (container) is updated' json = '{"webinfo": {"domain-name":"newdomain.com" ,"contact-email":"info@newdomain.com" }}' objectUnderTest.updateDataNodeAndDescendants(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', json, now, ContentType.JSON) - then: 'webinfo has been updated with teh new details' + then: 'webinfo has been updated with the new details' def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/webinfo', DIRECT_CHILDREN_ONLY) result.leaves.'domain-name'[0] == 'newdomain.com' result.leaves.'contact-email'[0] == 'info@newdomain.com' @@ -439,6 +439,27 @@ class DataServiceIntegrationSpec extends FunctionalSpecBase { restoreBookstoreDataAnchor(1) } + def 'Update list items.'() { + given: 'list of books' + def existingJsonData = '{"books": [ {"title":"Existing Book", "lang":"English"}, {"title":"Another existing book", "lang":"French"} ] }' + objectUnderTest.saveListElements(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2 , '/bookstore/categories[@code=\'1\']', existingJsonData, now, ContentType.JSON) + when: 'the books list is updated' + def updatedJsonData = '{"books": [ {"title":"Existing Book", "lang":"German"}, {"title":"A new book", "lang":"Hindi"} ] }' + objectUnderTest.updateDataNodeAndDescendants(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, '/bookstore/categories[@code=\'1\']', updatedJsonData, now, ContentType.JSON) + then: 'the expected number of updated books are retrieved' + def result = objectUnderTest.getDataNodesForMultipleXpaths(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_2, ['/bookstore/categories[@code=\'1\']/books[@title="Existing Book"]', '/bookstore/categories[@code=\'1\']/books[@title="A new book"]', '/bookstore/categories[@code=\'1\']/books[@title="Another existing book"]'], DIRECT_CHILDREN_ONLY) + assert result.size() == 2 + and: 'the updated books have expected xpaths' + def xpaths = result*.xpath + assert xpaths.containsAll(["/bookstore/categories[@code='1']/books[@title='A new book']", "/bookstore/categories[@code='1']/books[@title='Existing Book']"]) + and: 'the updated book has expected leaf value' + result[1].leaves['lang'] == 'German' + and: 'the book that was removed in the updated data is no longer present' + assert result.every { it.xpath != "/bookstore/categories[@code='1']/books[@title='Another existing book']" } + cleanup: + restoreBookstoreDataAnchor(2) + } + def 'Update bookstore top-level container data node.'() { when: 'the bookstore top-level container is updated' def json = '{ "bookstore": { "bookstore-name": "new bookstore" }}' -- 2.16.6