From: Arpit Singh Date: Thu, 30 Oct 2025 13:06:16 +0000 (+0530) Subject: Part 2: Add delta report in Kafka notification X-Git-Url: https://gerrit.onap.org/r/gitweb?a=commitdiff_plain;h=f50c80c8f8ff345106548d633e11073f7bf4e8d0;p=cps.git Part 2: Add delta report in Kafka notification Added support for delta report in kafka notifications for CPS core endpoints. The following methods are supported in this path: - saveData, saveListElements, updateNodeLeavesAndExistingDescendantLeaves, updateNodeLeaves, updateDataNodeAndDescendants, replaceListContent, deleteDataNode, deleteListOrListElement, deleteDataNodes. The following methods are not supported in this patch: - updateDataNodesAndDescendants, deleteDataNodes As they need a separate method to generate delta report and then send the notifications, which can be done as a separate patch. Issue-ID: CPS-3026 Change-Id: I28700c67df55e8e19754d7ca9e9a1aab5540fc71 Signed-off-by: Arpit Singh --- diff --git a/cps-application/src/main/resources/application.yml b/cps-application/src/main/resources/application.yml index e3e4261eac..743da6db44 100644 --- a/cps-application/src/main/resources/application.yml +++ b/cps-application/src/main/resources/application.yml @@ -121,6 +121,7 @@ app: data-updated: change-event-notifications-enabled: ${CPS_CHANGE_EVENT_NOTIFICATIONS_ENABLED:false} topic: ${CPS_CHANGE_EVENT_TOPIC:cps-data-updated-events} + delta-notification: ${CPS_DELTA_NOTIFICATION_ENABLED:false} notification: enabled: true diff --git a/cps-events/src/main/resources/schemas/cps.dataupdated/cps-data-updated-event-schema-1.0.0.json b/cps-events/src/main/resources/schemas/cps.dataupdated/cps-data-updated-event-schema-1.0.0.json index b15ba8bc2c..e094f76e8e 100644 --- a/cps-events/src/main/resources/schemas/cps.dataupdated/cps-data-updated-event-schema-1.0.0.json +++ b/cps-events/src/main/resources/schemas/cps.dataupdated/cps-data-updated-event-schema-1.0.0.json @@ -4,7 +4,7 @@ "$ref": "#/definitions/CpsDataUpdateEvent", "definitions": { "CpsDataUpdateEvent": { - "description": "The payload for CPS data updated event.", + "description": "The payload for CPS data update event.", "type": "object", "javaType": "org.onap.cps.events.model.CpsDataUpdatedEvent", "properties": { @@ -39,6 +39,24 @@ "xpath": { "description": "xpath of the modified content", "type": "string" + }, + "cloudEventData": { + "description": "Source and target data from delta report", + "type": "object", + "properties": { + "sourceData": { + "description": "Source data from delta report", + "type": "string", + "minLength": 1 + }, + "targetData": { + "description": "Target data from delta report", + "type": "string", + "minLength": 1 + } + }, + "required": ["source", "target"], + "additionalProperties": false } }, "required": [ diff --git a/cps-service/src/main/java/org/onap/cps/events/CpsDataUpdateEventsProducer.java b/cps-service/src/main/java/org/onap/cps/events/CpsDataUpdateEventsProducer.java index 5cd4f31f7b..2e8dcb8ddd 100644 --- a/cps-service/src/main/java/org/onap/cps/events/CpsDataUpdateEventsProducer.java +++ b/cps-service/src/main/java/org/onap/cps/events/CpsDataUpdateEventsProducer.java @@ -26,15 +26,20 @@ import static org.onap.cps.events.model.EventPayload.Action.fromValue; import io.cloudevents.CloudEvent; import io.micrometer.core.annotation.Timed; import java.time.OffsetDateTime; -import java.util.HashMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.onap.cps.api.CpsNotificationService; import org.onap.cps.api.model.Anchor; +import org.onap.cps.api.model.DeltaReport; +import org.onap.cps.events.model.CloudEventData; import org.onap.cps.events.model.CpsDataUpdatedEvent; import org.onap.cps.events.model.EventPayload; import org.onap.cps.utils.DateTimeUtility; +import org.onap.cps.utils.JsonObjectMapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -44,7 +49,7 @@ import org.springframework.stereotype.Service; public class CpsDataUpdateEventsProducer { private final EventProducer eventProducer; - + private final JsonObjectMapper jsonObjectMapper; private final CpsNotificationService cpsNotificationService; @Value("${app.cps.data-updated.topic:cps-data-updated-events}") @@ -65,20 +70,24 @@ public class CpsDataUpdateEventsProducer { * @param observedTimestamp timestamp when data was updated. */ @Timed(value = "cps.data.update.events.send", description = "Time taken to send Data Update event") - public void sendCpsDataUpdateEvent(final Anchor anchor, final String xpath, - final String action, final OffsetDateTime observedTimestamp) { + public void sendCpsDataUpdateEvent(final Anchor anchor, final String xpath, final String action, + final List deltaReports, final OffsetDateTime observedTimestamp) { if (notificationsEnabled && cpsChangeEventNotificationsEnabled && isNotificationEnabledForAnchor(anchor)) { - final CpsDataUpdatedEvent cpsDataUpdatedEvent = - createCpsDataUpdatedEvent(anchor, observedTimestamp, xpath, action); final String updateEventId = anchor.getDataspaceName() + ":" + anchor.getName(); - final Map extensions = createUpdateEventExtensions(updateEventId); - final CloudEvent cpsDataUpdatedEventAsCloudEvent = - CpsEvent.builder().type(CpsDataUpdatedEvent.class.getTypeName()).data(cpsDataUpdatedEvent) - .extensions(extensions).build().asCloudEvent(); - eventProducer.sendCloudEvent(topicName, updateEventId, cpsDataUpdatedEventAsCloudEvent); + final Map extensions = Map.of("correlationid", updateEventId); + if (!deltaReports.isEmpty()) { + final Collection cpsDataUpdatedEvents = + createUpdatedEventsFromDeltaReport(anchor, observedTimestamp, deltaReports); + cpsDataUpdatedEvents.forEach(cpsDataUpdatedEvent -> + sendCpsDataUpdatedEvent(updateEventId, cpsDataUpdatedEvent, extensions)); + } else { + final CpsDataUpdatedEvent cpsDataUpdatedEvent = + createCpsDataUpdatedEvent(anchor, observedTimestamp, xpath, action); + sendCpsDataUpdatedEvent(updateEventId, cpsDataUpdatedEvent, extensions); + } } else { log.debug("State of Overall Notifications : {} and Cps Change Event Notifications : {}", - notificationsEnabled, cpsChangeEventNotificationsEnabled); + notificationsEnabled, cpsChangeEventNotificationsEnabled); } } @@ -100,9 +109,43 @@ public class CpsDataUpdateEventsProducer { return cpsDataUpdatedEvent; } - private Map createUpdateEventExtensions(final String eventKey) { - final Map extensions = new HashMap<>(); - extensions.put("correlationid", eventKey); - return extensions; + private Collection createUpdatedEventsFromDeltaReport(final Anchor anchor, + final OffsetDateTime observedTimestamp, + final List deltaReports) { + final Collection cpsDataUpdatedEvents = new ArrayList<>(); + for (final DeltaReport deltaReport : deltaReports) { + if (deltaReport.getSourceData() != null || deltaReport.getTargetData() != null) { + cpsDataUpdatedEvents.add(toCpsDataUpdatedEvent(anchor, observedTimestamp, deltaReport)); + } + } + return cpsDataUpdatedEvents; + } + + private void sendCpsDataUpdatedEvent(final String updateEventId, + final CpsDataUpdatedEvent cpsDataUpdatedEvent, + final Map extensions) { + final CloudEvent cpsDataUpdatedEventAsCloudEvent = + CpsEvent.builder().type(CpsDataUpdatedEvent.class.getTypeName()).data(cpsDataUpdatedEvent) + .extensions(extensions).build().asCloudEvent(); + eventProducer.sendCloudEvent(topicName, updateEventId, cpsDataUpdatedEventAsCloudEvent); + } + + private CpsDataUpdatedEvent toCpsDataUpdatedEvent(final Anchor anchor, final OffsetDateTime observedTimestamp, + final DeltaReport deltaReport) { + final CloudEventData cloudEventData = new CloudEventData(); + cloudEventData.setSourceData(jsonObjectMapper.asJsonString(deltaReport.getSourceData())); + cloudEventData.setTargetData(jsonObjectMapper.asJsonString(deltaReport.getTargetData())); + final EventPayload updateEventData = new EventPayload(); + updateEventData.setObservedTimestamp(DateTimeUtility.toString(observedTimestamp)); + updateEventData.setDataspaceName(anchor.getDataspaceName()); + updateEventData.setAnchorName(anchor.getName()); + updateEventData.setSchemaSetName(anchor.getSchemaSetName()); + updateEventData.setXpath(deltaReport.getXpath()); + updateEventData.setAction(fromValue(deltaReport.getAction())); + updateEventData.setCloudEventData(cloudEventData); + + final CpsDataUpdatedEvent cpsDataUpdatedEvent = new CpsDataUpdatedEvent(); + cpsDataUpdatedEvent.setEventPayload(updateEventData); + return cpsDataUpdatedEvent; } } 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 e3da6c59da..c79a377e7b 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 @@ -24,6 +24,7 @@ package org.onap.cps.impl; +import static org.onap.cps.api.parameters.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS; 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; @@ -37,6 +38,7 @@ import java.io.Serializable; import java.time.OffsetDateTime; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -46,6 +48,7 @@ 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.api.parameters.FetchDescendantsOption; import org.onap.cps.cpspath.parser.CpsPathUtil; import org.onap.cps.events.CpsDataUpdateEventsProducer; @@ -53,6 +56,8 @@ import org.onap.cps.spi.CpsDataPersistenceService; import org.onap.cps.utils.ContentType; import org.onap.cps.utils.CpsValidator; import org.onap.cps.utils.YangParser; +import org.onap.cps.utils.deltareport.GroupedDeltaReportGenerator; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service @@ -64,6 +69,7 @@ public class CpsDataServiceImpl implements CpsDataService { private static final String CREATE_ACTION = CREATE.value(); private static final String REMOVE_ACTION = REMOVE.value(); private static final String REPLACE_ACTION = REPLACE.value(); + private static final List NO_DELTA_REPORTS = Collections.emptyList(); private final CpsDataPersistenceService cpsDataPersistenceService; private final CpsDataUpdateEventsProducer cpsDataUpdateEventsProducer; @@ -71,6 +77,10 @@ public class CpsDataServiceImpl implements CpsDataService { private final DataNodeFactory dataNodeFactory; private final CpsValidator cpsValidator; private final YangParser yangParser; + private final GroupedDeltaReportGenerator groupedDeltaReportGenerator; + + @Value("${app.cps.data-updated.delta-notification:false}") + private boolean deltaNotificationEnabled; @Override public void saveData(final String dataspaceName, final String anchorName, final String nodeData, @@ -86,8 +96,10 @@ public class CpsDataServiceImpl implements CpsDataService { final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); final Collection dataNodes = dataNodeFactory .createDataNodesWithAnchorParentXpathAndNodeData(anchor, ROOT_NODE_XPATH, nodeData, contentType); + final List deltaReports = + generateDeltaReports(dataspaceName, anchorName, ROOT_NODE_XPATH, dataNodes); cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, dataNodes); - sendDataUpdatedEvent(anchor, ROOT_NODE_XPATH, CREATE_ACTION, observedTimestamp); + sendDataUpdatedEvent(anchor, ROOT_NODE_XPATH, CREATE_ACTION, deltaReports, observedTimestamp); } @Override @@ -105,8 +117,10 @@ public class CpsDataServiceImpl implements CpsDataService { final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); final Collection dataNodes = dataNodeFactory .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); + final List deltaReports = + generateDeltaReports(dataspaceName, anchorName, parentNodeXpath, dataNodes); cpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes); - sendDataUpdatedEvent(anchor, parentNodeXpath, CREATE_ACTION, observedTimestamp); + sendDataUpdatedEvent(anchor, parentNodeXpath, CREATE_ACTION, deltaReports, observedTimestamp); } @Override @@ -118,13 +132,15 @@ public class CpsDataServiceImpl implements CpsDataService { final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); final Collection listElementDataNodeCollection = dataNodeFactory .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); + final List deltaReports = + generateDeltaReports(dataspaceName, anchorName, parentNodeXpath, listElementDataNodeCollection); if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, listElementDataNodeCollection); } else { cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath, listElementDataNodeCollection); } - sendDataUpdatedEvent(anchor, parentNodeXpath, REPLACE_ACTION, observedTimestamp); + sendDataUpdatedEvent(anchor, parentNodeXpath, REPLACE_ACTION, deltaReports, observedTimestamp); } @Override @@ -155,10 +171,12 @@ public class CpsDataServiceImpl implements CpsDataService { final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); final Collection dataNodesInPatch = dataNodeFactory .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); + final List deltaReports = + generateDeltaReports(dataspaceName, anchorName, parentNodeXpath, dataNodesInPatch); final Map> xpathToUpdatedLeaves = dataNodesInPatch.stream() .collect(Collectors.toMap(DataNode::getXpath, DataNode::getLeaves)); cpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, xpathToUpdatedLeaves); - sendDataUpdatedEvent(anchor, parentNodeXpath, REPLACE_ACTION, observedTimestamp); + sendDataUpdatedEvent(anchor, parentNodeXpath, REPLACE_ACTION, deltaReports, observedTimestamp); } @Override @@ -173,10 +191,12 @@ public class CpsDataServiceImpl implements CpsDataService { final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); final Collection dataNodeUpdates = dataNodeFactory .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, dataNodeUpdatesAsJson, JSON); + final List deltaReports = + generateDeltaReports(dataspaceName, anchorName, parentNodeXpath, dataNodeUpdates); for (final DataNode dataNodeUpdate : dataNodeUpdates) { processDataNodeUpdate(anchor, dataNodeUpdate); } - sendDataUpdatedEvent(anchor, parentNodeXpath, REPLACE_ACTION, observedTimestamp); + sendDataUpdatedEvent(anchor, parentNodeXpath, REPLACE_ACTION, deltaReports, observedTimestamp); } @Override @@ -210,12 +230,14 @@ public class CpsDataServiceImpl implements CpsDataService { final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); final Collection dataNodes = dataNodeFactory .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, nodeData, contentType); + final List deltaReports = + generateDeltaReports(dataspaceName, anchorName, parentNodeXpath, 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); + sendDataUpdatedEvent(anchor, parentNodeXpath, REPLACE_ACTION, deltaReports, observedTimestamp); } @Override @@ -230,7 +252,7 @@ public class CpsDataServiceImpl implements CpsDataService { .createDataNodesWithAnchorAndXpathToNodeData(anchor, nodeDataPerParentNodeXPath, contentType); cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes); nodeDataPerParentNodeXPath.keySet().forEach(nodeXpath -> - sendDataUpdatedEvent(anchor, nodeXpath, REPLACE_ACTION, observedTimestamp)); + sendDataUpdatedEvent(anchor, nodeXpath, REPLACE_ACTION, NO_DELTA_REPORTS, observedTimestamp)); } @Override @@ -250,8 +272,10 @@ public class CpsDataServiceImpl implements CpsDataService { final Collection dataNodes, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); + final List deltaReports = + generateDeltaReports(dataspaceName, anchorName, parentNodeXpath, dataNodes); cpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, parentNodeXpath, dataNodes); - sendDataUpdatedEvent(anchor, parentNodeXpath, REPLACE_ACTION, observedTimestamp); + sendDataUpdatedEvent(anchor, parentNodeXpath, REPLACE_ACTION, deltaReports, observedTimestamp); } @Override @@ -259,9 +283,11 @@ public class CpsDataServiceImpl implements CpsDataService { public void deleteDataNode(final String dataspaceName, final String anchorName, final String dataNodeXpath, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); + final List deltaReports = generateDeltaReports(dataspaceName, anchorName, dataNodeXpath, + Collections.emptyList()); cpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, dataNodeXpath); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - sendDataUpdatedEvent(anchor, dataNodeXpath, REMOVE_ACTION, observedTimestamp); + sendDataUpdatedEvent(anchor, dataNodeXpath, REMOVE_ACTION, deltaReports, observedTimestamp); } @Override @@ -272,7 +298,7 @@ public class CpsDataServiceImpl implements CpsDataService { cpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName, dataNodeXpaths); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); dataNodeXpaths.forEach(dataNodeXpath -> - sendDataUpdatedEvent(anchor, dataNodeXpath, REMOVE_ACTION, observedTimestamp)); + sendDataUpdatedEvent(anchor, dataNodeXpath, REMOVE_ACTION, NO_DELTA_REPORTS, observedTimestamp)); } @@ -282,9 +308,11 @@ public class CpsDataServiceImpl implements CpsDataService { public void deleteDataNodes(final String dataspaceName, final String anchorName, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); + final List deltaReports = generateDeltaReports(dataspaceName, anchorName, ROOT_NODE_XPATH, + Collections.emptyList()); cpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - sendDataUpdatedEvent(anchor, ROOT_NODE_XPATH, REMOVE_ACTION, observedTimestamp); + sendDataUpdatedEvent(anchor, ROOT_NODE_XPATH, REMOVE_ACTION, deltaReports, observedTimestamp); } @Override @@ -292,11 +320,12 @@ public class CpsDataServiceImpl implements CpsDataService { description = "Time taken to delete all data nodes for multiple anchors") public void deleteDataNodes(final String dataspaceName, final Collection anchorNames, final OffsetDateTime observedTimestamp) { + final boolean deltaNotification = false; cpsValidator.validateNameCharacters(dataspaceName); cpsValidator.validateNameCharacters(anchorNames); cpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorNames); for (final Anchor anchor : cpsAnchorService.getAnchors(dataspaceName, anchorNames)) { - sendDataUpdatedEvent(anchor, ROOT_NODE_XPATH, REMOVE_ACTION, observedTimestamp); + sendDataUpdatedEvent(anchor, ROOT_NODE_XPATH, REMOVE_ACTION, NO_DELTA_REPORTS, observedTimestamp); } } @@ -305,9 +334,11 @@ public class CpsDataServiceImpl implements CpsDataService { public void deleteListOrListElement(final String dataspaceName, final String anchorName, final String listNodeXpath, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); + final List deltaReports = generateDeltaReports(dataspaceName, anchorName, listNodeXpath, + Collections.emptyList()); cpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, listNodeXpath); final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName); - sendDataUpdatedEvent(anchor, listNodeXpath, REMOVE_ACTION, observedTimestamp); + sendDataUpdatedEvent(anchor, listNodeXpath, REMOVE_ACTION, deltaReports, observedTimestamp); } @Override @@ -328,12 +359,23 @@ public class CpsDataServiceImpl implements CpsDataService { } } + private List generateDeltaReports(final String dataspaceName, final String anchorName, + final String xpath, final Collection targetDataNodes) { + if (deltaNotificationEnabled) { + final Collection sourceDataNodes = getDataNodesForMultipleXpaths(dataspaceName, anchorName, + Collections.singletonList(xpath), INCLUDE_ALL_DESCENDANTS); + return groupedDeltaReportGenerator.createCondensedDeltaReports(sourceDataNodes, targetDataNodes); + } + return NO_DELTA_REPORTS; + } + private void sendDataUpdatedEvent(final Anchor anchor, final String xpath, final String action, + final List deltaReports, final OffsetDateTime observedTimestamp) { try { - cpsDataUpdateEventsProducer.sendCpsDataUpdateEvent(anchor, xpath, action, observedTimestamp); + cpsDataUpdateEventsProducer.sendCpsDataUpdateEvent(anchor, xpath, action, deltaReports, observedTimestamp); } catch (final Exception exception) { log.error("Failed to send message to notification service", exception); } diff --git a/cps-service/src/test/groovy/org/onap/cps/events/CpsDataUpdateEventProducerSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/events/CpsDataUpdateEventProducerSpec.groovy index 1b3e2600a4..aa20bd19f2 100644 --- a/cps-service/src/test/groovy/org/onap/cps/events/CpsDataUpdateEventProducerSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/events/CpsDataUpdateEventProducerSpec.groovy @@ -28,6 +28,7 @@ import io.cloudevents.jackson.PojoCloudEventDataMapper import org.onap.cps.api.CpsNotificationService import org.onap.cps.api.model.Anchor import org.onap.cps.events.model.CpsDataUpdatedEvent +import org.onap.cps.impl.DeltaReportBuilder import org.onap.cps.utils.JsonObjectMapper import org.springframework.test.context.ContextConfiguration import spock.lang.Specification @@ -48,34 +49,37 @@ class CpsDataUpdateEventProducerSpec extends Specification { def mockEventProducer = Mock(EventProducer) def objectMapper = new ObjectMapper(); def mockCpsNotificationService = Mock(CpsNotificationService) + def jsonObjectMapper = new JsonObjectMapper(objectMapper) - def objectUnderTest = new CpsDataUpdateEventsProducer(mockEventProducer, mockCpsNotificationService) + def objectUnderTest = new CpsDataUpdateEventsProducer(mockEventProducer, jsonObjectMapper, mockCpsNotificationService) def setup() { mockCpsNotificationService.isNotificationEnabled('dataspace01', 'anchor01') >> true objectUnderTest.topicName = 'cps-core-event' } + static def deltaReport = [] + def 'Create and send cps event with #scenario.'() { given: 'an anchor' - def anchor = new Anchor('anchor01', 'dataspace01', 'schema01'); + def anchor = new Anchor('anchor01', 'dataspace01', 'schema01') and: 'notificationsEnabled is #notificationsEnabled and it will be true as default' objectUnderTest.notificationsEnabled = true and: 'cpsChangeEventNotificationsEnabled is also true' objectUnderTest.cpsChangeEventNotificationsEnabled = true when: 'service is called to send data update event' - objectUnderTest.sendCpsDataUpdateEvent(anchor, xpath, actionInRequest, OffsetDateTime.now()) + objectUnderTest.sendCpsDataUpdateEvent(anchor, xpath, actionInRequest, deltaReport, OffsetDateTime.now()) then: 'the event contains the required attributes' 1 * mockEventProducer.sendCloudEvent('cps-core-event', 'dataspace01:anchor01', _) >> { - args -> - { - def cpsDataUpdatedEvent = (args[2] as CloudEvent) - assert cpsDataUpdatedEvent.getExtension('correlationid') == 'dataspace01:anchor01' - assert cpsDataUpdatedEvent.type == 'org.onap.cps.events.model.CpsDataUpdatedEvent' - assert cpsDataUpdatedEvent.source.toString() == 'CPS' - def actualEventOperation = CloudEventUtils.mapData(cpsDataUpdatedEvent, PojoCloudEventDataMapper.from(objectMapper, CpsDataUpdatedEvent.class)).getValue().eventPayload.action.value() - assert actualEventOperation == expectedAction - } + args -> + { + def cpsDataUpdatedEvent = (args[2] as CloudEvent) + assert cpsDataUpdatedEvent.getExtension('correlationid') == 'dataspace01:anchor01' + assert cpsDataUpdatedEvent.type == 'org.onap.cps.events.model.CpsDataUpdatedEvent' + assert cpsDataUpdatedEvent.source.toString() == 'CPS' + def actualEventOperation = CloudEventUtils.mapData(cpsDataUpdatedEvent, PojoCloudEventDataMapper.from(objectMapper, CpsDataUpdatedEvent.class)).getValue().eventPayload.action.value() + assert actualEventOperation == expectedAction + } } where: 'the following values are used' scenario | xpath | actionInRequest || expectedAction @@ -91,20 +95,20 @@ class CpsDataUpdateEventProducerSpec extends Specification { def 'Send cps event when no timestamp provided.'() { given: 'an anchor' - def anchor = new Anchor('anchor01', 'dataspace01', 'schema01'); + def anchor = new Anchor('anchor01', 'dataspace01', 'schema01') and: 'notificationsEnabled is true' objectUnderTest.notificationsEnabled = true and: 'cpsChangeEventNotificationsEnabled is true' objectUnderTest.cpsChangeEventNotificationsEnabled = true when: 'service is called to send data event' - objectUnderTest.sendCpsDataUpdateEvent(anchor, '/', CREATE_ACTION, null) + objectUnderTest.sendCpsDataUpdateEvent(anchor, '/', CREATE_ACTION, deltaReport, null) then: 'the event is sent' 1 * mockEventProducer.sendCloudEvent('cps-core-event', 'dataspace01:anchor01', _) } def 'Enabling and disabling sending cps events.'() { given: 'an anchor' - def anchor = new Anchor('anchor02', 'some dataspace', 'some schema'); + def anchor = new Anchor('anchor02', 'some dataspace', 'some schema') and: 'notificationsEnabled is #notificationsEnabled' objectUnderTest.notificationsEnabled = notificationsEnabled and: 'cpsChangeEventNotificationsEnabled is #cpsChangeEventNotificationsEnabled' @@ -112,15 +116,64 @@ class CpsDataUpdateEventProducerSpec extends Specification { and: 'notification service enabled is: #cpsNotificationServiceisNotificationEnabled' mockCpsNotificationService.isNotificationEnabled(_, 'anchor02') >> cpsNotificationServiceisNotificationEnabled when: 'service is called to send data event' - objectUnderTest.sendCpsDataUpdateEvent(anchor, '/', CREATE_ACTION, null) + objectUnderTest.sendCpsDataUpdateEvent(anchor, '/', CREATE_ACTION, deltaReport, OffsetDateTime.now()) then: 'the event is only sent when all related flags are true' expectedCallsToProducer * mockEventProducer.sendCloudEvent(*_) where: 'the following flags are used' - notificationsEnabled | cpsChangeEventNotificationsEnabled | cpsNotificationServiceisNotificationEnabled || expectedCallsToProducer - false | true | true || 0 - true | false | true || 0 - true | true | false || 0 - true | true | true || 1 + notificationsEnabled | cpsChangeEventNotificationsEnabled | cpsNotificationServiceisNotificationEnabled || expectedCallsToProducer + false | true | true || 0 + true | false | true || 0 + true | true | false || 0 + true | true | true || 1 + } + + def 'Sending CPS event with delta report'() { + given: 'an anchor' + def anchor = new Anchor('anchor01', 'dataspace01', 'schema01') + and: 'general notifications and cps change event notifications are enabled' + objectUnderTest.notificationsEnabled = true + objectUnderTest.cpsChangeEventNotificationsEnabled = true + when: 'service is called to send data event' + objectUnderTest.sendCpsDataUpdateEvent(anchor, '/', REPLACE.value(), deltaReports, OffsetDateTime.now()) + then: 'the cloud event producer is invoked and an event is sent for each entry in delta report' + deltaReports.forEach { deltaReport -> + expectedInvocationCount * mockEventProducer.sendCloudEvent('cps-core-event', 'dataspace01:anchor01', _) >> { + args -> + { + def cpsDataUpdatedEvent = (args[2] as CloudEvent) + def eventPayload = CloudEventUtils.mapData(cpsDataUpdatedEvent, PojoCloudEventDataMapper.from(objectMapper, CpsDataUpdatedEvent.class)).getValue().eventPayload + assert eventPayload.action.value() == deltaReport.getAction() + assert eventPayload.xpath == deltaReport.xpath + assert deltaReport.action == expectedEventAction + } + } + } + where: 'the following values are used' + scenario | deltaReports || expectedInvocationCount | expectedEventAction + 'delta report with source data' | [new DeltaReportBuilder().actionRemove().withXpath('/bookstore').withSourceData(['categories': ['code': '4', 'name': 'Computing']]).build()] || 1 | REMOVE_ACTION + 'delta report with target data' | [new DeltaReportBuilder().actionCreate().withXpath('/bookstore').withTargetData(['categories': ['code': '4', 'name': 'Computing']]).build()] || 1 | CREATE_ACTION + 'delta report with no source or target data' | [new DeltaReportBuilder().build()] || 0 | null + 'delta report with source and target data' | [new DeltaReportBuilder().actionReplace().withXpath('/bookstore').withSourceData(['categories': ['code': '4', 'name': 'Computing']]).withTargetData(['categories': [['code': '4', 'name': 'Funny']]]).build()] || 1 | REPLACE_ACTION } -} + def 'Sending legacy CPS event when delta report is empty'() { + given: 'an anchor and empty delta reports' + def anchor = new Anchor('anchor01', 'dataspace01', 'schema01') + def emptyDeltaReports = [] + and: 'general notifications and cps change event notifications are enabled' + objectUnderTest.notificationsEnabled = true + objectUnderTest.cpsChangeEventNotificationsEnabled = true + when: 'attempt to send data update event' + objectUnderTest.sendCpsDataUpdateEvent(anchor, '/', CREATE_ACTION, emptyDeltaReports, OffsetDateTime.now()) + then: 'the cloud event producer is invoked once for legacy event' + 1 * mockEventProducer.sendCloudEvent('cps-core-event', 'dataspace01:anchor01', _) >> { + args -> + { + def cpsDataUpdatedEvent = (args[2] as CloudEvent) + def eventPayload = CloudEventUtils.mapData(cpsDataUpdatedEvent, PojoCloudEventDataMapper.from(objectMapper, CpsDataUpdatedEvent.class)).getValue().eventPayload + assert eventPayload.action.value() == CREATE_ACTION + assert eventPayload.xpath == '/' + } + } + } +} \ No newline at end of file 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 1aedb74882..665f1d79ef 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 @@ -41,6 +41,7 @@ import org.onap.cps.utils.ContentType import org.onap.cps.utils.CpsValidator import org.onap.cps.utils.YangParser import org.onap.cps.utils.YangParserHelper +import org.onap.cps.utils.deltareport.GroupedDeltaReportGenerator import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder import org.onap.cps.yang.YangTextSchemaSourceSet import org.onap.cps.yang.YangTextSchemaSourceSetBuilder @@ -60,9 +61,10 @@ class CpsDataServiceImplSpec extends Specification { def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, mockTimedYangTextSchemaSourceSetBuilder) def mockCpsDataUpdateEventsProducer = Mock(CpsDataUpdateEventsProducer) def dataNodeFactory = new DataNodeFactoryImpl(yangParser) + def mockGroupedDeltaReportGenerator = Mock(GroupedDeltaReportGenerator) def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsDataUpdateEventsProducer, mockCpsAnchorService, - dataNodeFactory, mockCpsValidator, yangParser) + dataNodeFactory, mockCpsValidator, yangParser, mockGroupedDeltaReportGenerator) def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class) def loggingListAppender @@ -578,7 +580,7 @@ class CpsDataServiceImplSpec extends Specification { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') when: 'producer throws an exception while sending event' - mockCpsDataUpdateEventsProducer.sendCpsDataUpdateEvent(_, _, _, _) >> { throw new Exception("Sending failed")} + mockCpsDataUpdateEventsProducer.sendCpsDataUpdateEvent(*_) >> { throw new Exception("Sending failed")} and: 'an update event is performed' objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/', '{"test-tree": {"branch": []}}', observedTimestamp, ContentType.JSON) then: 'the exception is not bubbled up' @@ -588,6 +590,213 @@ class CpsDataServiceImplSpec extends Specification { assert logs.contains('Failed to send message to notification service') } + def 'Save data nodes with delta notifications enabled'() { + given: 'schema set for given anchor and dataspace references test-tree model, json data and a delta report' + setupSchemaSetMocks('test-tree.yang') + def jsonData = '{"test-tree": {"branch": []}}' + def deltaReports = [new DeltaReportBuilder().withXpath('/').actionCreate().withTargetData(['name': 'Right']).build()] + and: 'delta report in notifications is enabled' + def deltaNotificationEnabled = (objectUnderTest.deltaNotificationEnabled = true) + when: 'save data method is invoked with json data' + objectUnderTest.saveData(dataspaceName, anchorName, jsonData, observedTimestamp) + then: 'the delta report generator returns delta reports' + mockGroupedDeltaReportGenerator.createCondensedDeltaReports(*_) >> deltaReports + and: 'the persistence service method is invoked with correct parameters' + 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, _) + and: 'the event producer is invoked with correct parameters including the correct delta reports' + 1 * mockCpsDataUpdateEventsProducer.sendCpsDataUpdateEvent(anchor, '/', 'create', { deltaReport -> + assert deltaReport.size() == 1 + assert deltaReport[0].xpath == '/' + assert deltaReport[0].targetData == ['name': 'Right'] + }, observedTimestamp) + } + + def 'Save data nodes with xpath and delta report in notifications enabled'() { + given: 'schema set for given anchor and dataspace references test-tree model, json data and a delta report' + setupSchemaSetMocks('test-tree.yang') + def jsonData = '{"branch": [ { "name" : "Left" }]}' + def deltaReports = [new DeltaReportBuilder().withXpath('/test-tree').actionCreate().withTargetData(['name': 'Right']).build()] + and: 'delta report in notifications is enabled' + def deltaNotificationEnabled = (objectUnderTest.deltaNotificationEnabled = true) + when: 'save data method is invoked with json data and xpath' + objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp) + then: 'the delta report generator returns delta reports' + mockGroupedDeltaReportGenerator.createCondensedDeltaReports(*_) >> deltaReports + and: 'the persistence service method is invoked with correct parameters' + 1 * mockCpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, '/test-tree', _) + and: 'the event producer is invoked with correct parameters including the correct delta reports' + 1 * mockCpsDataUpdateEventsProducer.sendCpsDataUpdateEvent(anchor, '/test-tree', 'create', { deltaReport -> + assert deltaReport.size() == 1 + assert deltaReport[0].xpath == '/test-tree' + assert deltaReport[0].targetData == ['name': 'Right'] + }, observedTimestamp) + } + + def 'Save list data node with delta report in notifications enabled'() { + given: 'schema set for given anchor and dataspace references test-tree model, json data and a delta report' + setupSchemaSetMocks('test-tree.yang') + def jsonData = '{"branch": [ { "name" : "Left" }]}' + def deltaReports = [new DeltaReportBuilder().withXpath('/test-tree/branch[@name=\'Left\']').actionCreate().withTargetData(['name': 'Left']).build()] + and: 'delta report in notifications is enabled' + def deltaNotificationEnabled = (objectUnderTest.deltaNotificationEnabled = true) + when: 'save list data method is invoked with json data' + objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp, ContentType.JSON) + then: 'the delta report generator returns delta reports' + mockGroupedDeltaReportGenerator.createCondensedDeltaReports(*_) >> deltaReports + and: 'the persistence service method is invoked with correct parameters' + 1 * mockCpsDataPersistenceService.addListElements(dataspaceName, anchorName, '/test-tree', _) + and: 'the event producer is invoked with correct parameters including the correct delta reports' + 1 * mockCpsDataUpdateEventsProducer.sendCpsDataUpdateEvent(anchor, '/test-tree', 'replace', { deltaReport -> + assert deltaReport.size() == 1 + assert deltaReport[0].xpath == '/test-tree/branch[@name=\'Left\']' + assert deltaReport[0].targetData == ['name': 'Left'] + }, observedTimestamp) + } + + def 'Update data node leaves and descendants with delta report in notifications enabled'() { + given: 'schema set for given anchor and dataspace references test-tree model, json data and a delta report' + setupSchemaSetMocks('test-tree.yang') + def jsonData = '{"branch": [ { "name" : "Left" }]}' + def deltaReports = [new DeltaReportBuilder().withXpath('/test-tree/branch[@name=\'Left\']').actionReplace().withTargetData(['name': 'Left']).build()] + and: 'delta report in notifications is enabled' + def deltaNotificationEnabled = (objectUnderTest.deltaNotificationEnabled = true) + when: 'update data method is invoked with json data' + objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp) + then: 'the delta report generator returns delta reports' + mockGroupedDeltaReportGenerator.createCondensedDeltaReports(*_) >> deltaReports + and: 'the persistence service method is invoked with correct parameters' + 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, _) + and: 'the event producer is invoked with correct parameters including the correct delta reports' + 1 * mockCpsDataUpdateEventsProducer.sendCpsDataUpdateEvent(anchor, '/test-tree', 'replace', { deltaReport -> + assert deltaReport.size() == 1 + assert deltaReport[0].xpath == '/test-tree/branch[@name=\'Left\']' + assert deltaReport[0].targetData == ['name': 'Left'] + }, observedTimestamp) + } + + def 'Update node leaves with delta report in notifications enabled'() { + given: 'schema set for given anchor and dataspace references test-tree model, json data and a delta report' + setupSchemaSetMocks('test-tree.yang') + def jsonData = '{"branch": [ { "name" : "Left" }]}' + def deltaReports = [new DeltaReportBuilder().withXpath('/test-tree/branch[@name=\'Left\']').actionReplace().withTargetData(['name': 'Left']).build()] + and: 'delta report in notifications is enabled' + def deltaNotificationEnabled = (objectUnderTest.deltaNotificationEnabled = true) + when: 'update data method is invoked with json data' + objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp, ContentType.JSON) + then: 'the delta report generator returns delta reports' + mockGroupedDeltaReportGenerator.createCondensedDeltaReports(*_) >> deltaReports + and: 'the persistence service method is invoked with correct parameters' + 1 * mockCpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, _) + and: 'the event producer is invoked with correct parameters including the correct delta reports' + 1 * mockCpsDataUpdateEventsProducer.sendCpsDataUpdateEvent(anchor, '/test-tree', 'replace', { deltaReport -> + assert deltaReport.size() == 1 + assert deltaReport[0].xpath == '/test-tree/branch[@name=\'Left\']' + assert deltaReport[0].targetData == ['name': 'Left'] + }, observedTimestamp) + } + + def 'Update data nodes and descendants with delta report in notifications enabled'() { + given: 'schema set for given anchor and dataspace references test-tree model, json data and a delta report' + setupSchemaSetMocks('test-tree.yang') + def jsonData = '{"branch": [ { "name" : "Left" }]}' + def deltaReports = [new DeltaReportBuilder().withXpath('/test-tree/branch[@name=\'Left\']').actionReplace().withTargetData(['name': 'Left']).build()] + and: 'delta report in notifications is enabled' + def deltaNotificationEnabled = (objectUnderTest.deltaNotificationEnabled = true) + when: 'update data method is invoked with json data' + objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp, ContentType.JSON) + then: 'the delta report generator returns delta reports' + mockGroupedDeltaReportGenerator.createCondensedDeltaReports(*_) >> deltaReports + and: 'the persistence service method is invoked with correct parameters' + 1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, _) + and: 'the event producer is invoked with correct parameters including the correct delta reports' + 1 * mockCpsDataUpdateEventsProducer.sendCpsDataUpdateEvent(anchor, '/test-tree', 'replace', { deltaReport -> + assert deltaReport.size() == 1 + assert deltaReport[0].xpath == '/test-tree/branch[@name=\'Left\']' + assert deltaReport[0].targetData == ['name': 'Left'] + }, observedTimestamp) + } + + def 'Replace list content with delta report in notifications enabled'() { + given: 'schema set for given anchor and dataspace references test-tree model, json data and a delta report' + setupSchemaSetMocks('test-tree.yang') + def jsonData = '{"branch": [ { "name" : "Left" }]}' + def deltaReports = [new DeltaReportBuilder().withXpath('/test-tree/branch[@name=\'Left\']').actionReplace().withTargetData(['name': 'Left']).build()] + and: 'delta report in notifications is enabled' + def deltaNotificationEnabled = (objectUnderTest.deltaNotificationEnabled = true) + when: 'replace list content method is invoked with json data' + objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp, ContentType.JSON) + then: 'the delta report generator returns delta reports' + mockGroupedDeltaReportGenerator.createCondensedDeltaReports(*_) >> deltaReports + and: 'the persistence service method is invoked with correct parameters' + 1 * mockCpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, '/test-tree', _) + and: 'the event producer is invoked with correct parameters including the correct delta reports' + 1 * mockCpsDataUpdateEventsProducer.sendCpsDataUpdateEvent(anchor, '/test-tree', 'replace', { deltaReport -> + assert deltaReport.size() == 1 + assert deltaReport[0].xpath == '/test-tree/branch[@name=\'Left\']' + assert deltaReport[0].targetData == ['name': 'Left'] + }, observedTimestamp) + } + + def 'Delete data node with delta report in notifications enabled'() { + given: 'schema set for given anchor and dataspace references test-tree model and a delta report' + setupSchemaSetMocks('test-tree.yang') + def deltaReports = [new DeltaReportBuilder().withXpath('/data-node').actionRemove().build()] + and: 'delta report in notifications is enabled' + def deltaNotificationEnabled = (objectUnderTest.deltaNotificationEnabled = true) + when: 'delete data node method is invoked with correct parameters' + objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp) + then: 'the delta report generator returns delta reports' + mockGroupedDeltaReportGenerator.createCondensedDeltaReports(*_) >> deltaReports + and: 'the persistence service method is invoked with the correct parameters' + 1 * mockCpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, '/data-node') + and: 'the event producer is invoked with correct parameters including the correct delta reports' + 1 * mockCpsDataUpdateEventsProducer.sendCpsDataUpdateEvent(anchor, '/data-node', 'remove', { deltaReport -> + assert deltaReport.size() == 1 + assert deltaReport[0].xpath == '/data-node' + }, observedTimestamp) + } + + def 'Delete list element with delta report in notifications enabled'() { + given: 'schema set for given anchor and dataspace references test-tree model and a delta report' + setupSchemaSetMocks('test-tree.yang') + def deltaReports = [new DeltaReportBuilder().withXpath('/test-tree/branch[@name="A"]').actionRemove().build()] + and: 'delta report in notifications is enabled' + def deltaNotificationEnabled = (objectUnderTest.deltaNotificationEnabled = true) + when: 'delete list data method is invoked with correct parameters' + objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch[@name="A"]', observedTimestamp) + then: 'the delta report generator returns delta reports' + mockGroupedDeltaReportGenerator.createCondensedDeltaReports(*_) >> deltaReports + and: 'the persistence service method is invoked with the correct parameters' + 1 * mockCpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, '/test-tree/branch[@name="A"]') + and: 'the event producer is invoked with correct parameters including the correct delta reports' + 1 * mockCpsDataUpdateEventsProducer.sendCpsDataUpdateEvent(anchor, '/test-tree/branch[@name="A"]', 'remove', { deltaReport -> + assert deltaReport.size() == 1 + assert deltaReport[0].xpath == '/test-tree/branch[@name="A"]' + }, observedTimestamp) + } + + def 'Delete data nodes with delta report in notifications enabled'() { + given: 'schema set for given anchor and dataspace references test-tree model and delta reports' + setupSchemaSetMocks('test-tree.yang') + def deltaReports = [ + new DeltaReportBuilder().withXpath('/test-tree/branch[@name=\'A\']').actionRemove().build(), + new DeltaReportBuilder().withXpath('/test-tree/branch[@name=\'B\']').actionRemove().build() + ] + and: 'delta report in notifications is enabled' + def deltaNotificationEnabled = (objectUnderTest.deltaNotificationEnabled = true) + when: 'delete data nodes method is invoked with correct parameters' + objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp) + then: 'the delta report generator returns delta reports' + mockGroupedDeltaReportGenerator.createCondensedDeltaReports(*_) >> deltaReports + and: 'the persistence service method is invoked with the correct parameters' + 1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName) + and: 'the event producer is invoked with correct parameters including the correct delta reports' + 1 * mockCpsDataUpdateEventsProducer.sendCpsDataUpdateEvent(anchor, '/', 'remove', { deltaReport -> + assert deltaReport.size() == 2 + assert deltaReport.collect { it.xpath }.containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']']) + }, observedTimestamp) + } + def setupSchemaSetMocks(String... yangResources) { def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet) mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy index 5906bae56c..64f44aaedf 100755 --- a/cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy @@ -33,6 +33,7 @@ import org.onap.cps.utils.ContentType import org.onap.cps.utils.CpsValidator import org.onap.cps.utils.YangParser import org.onap.cps.utils.YangParserHelper +import org.onap.cps.utils.deltareport.GroupedDeltaReportGenerator import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import spock.lang.Specification @@ -48,7 +49,8 @@ class E2ENetworkSliceSpec extends Specification { def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockCpsModulePersistenceService, mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder,yangParser) def mockCpsDataUpdateEventsProducer = Mock(CpsDataUpdateEventsProducer) def dataNodeFactory = new DataNodeFactoryImpl(yangParser) - def cpsDataServiceImpl = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsDataUpdateEventsProducer, mockCpsAnchorService, dataNodeFactory, mockCpsValidator, yangParser) + def mockGroupedDeltaReportGenerator = Mock(GroupedDeltaReportGenerator) + def cpsDataServiceImpl = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsDataUpdateEventsProducer, mockCpsAnchorService, dataNodeFactory, mockCpsValidator, yangParser, mockGroupedDeltaReportGenerator) def dataspaceName = 'someDataspace' def anchorName = 'someAnchor' def schemaSetName = 'someSchemaSet' diff --git a/docs/api/swagger/cps/openapi.yaml b/docs/api/swagger/cps/openapi.yaml index ebf3c63e30..0088b26557 100644 --- a/docs/api/swagger/cps/openapi.yaml +++ b/docs/api/swagger/cps/openapi.yaml @@ -1829,6 +1829,13 @@ paths: schema: type: object description: OK + "204": + content: + application/json: + schema: + example: my-resource + type: string + description: Created "400": content: application/json: