From 673c6d94830a1677e685cab82a76747a0808d347 Mon Sep 17 00:00:00 2001 From: aditya puthuparambil Date: Tue, 24 Aug 2021 17:44:34 +0100 Subject: Add optional observed timestamp in the cps data api - Added optional query parameter in cps data endpoints - Updated service layer and notification to use observedTimestamp Note: - NCMP REST endpoints are not updated as a part of this patch - NCMP does not sent observed timestamp when using cps data services Issue-ID: CPS-477 Signed-off-by: puthuparambil.aditya Change-Id: I1f92da3da7b3a13c45405fdf44e5fef861991d9a Signed-off-by: Renu Kumari --- .../main/java/org/onap/cps/api/CpsDataService.java | 66 +++++++++++-------- .../org/onap/cps/api/impl/CpsDataServiceImpl.java | 43 +++++++------ .../notification/CpsDataUpdatedEventFactory.java | 25 +++++--- .../onap/cps/notification/NotificationService.java | 7 +- .../cps/api/impl/CpsDataServiceImplSpec.groovy | 75 ++++++++++++---------- .../onap/cps/api/impl/E2ENetworkSliceSpec.groovy | 6 +- .../CpsDataUpdateEventFactorySpec.groovy | 37 +++++++---- .../notification/NotificationServiceSpec.groovy | 17 +++-- .../java/org/onap/cps/utils/DateTimeUtility.java | 40 ++++++++++++ 9 files changed, 203 insertions(+), 113 deletions(-) create mode 100644 cps-service/src/test/java/org/onap/cps/utils/DateTimeUtility.java (limited to 'cps-service') diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java index 2583e9905b..31a7517340 100644 --- a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java @@ -2,6 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (C) 2020 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech + * Modifications Copyright (C) 2021 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +22,7 @@ package org.onap.cps.api; +import java.time.OffsetDateTime; import org.checkerframework.checker.nullness.qual.NonNull; import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.model.DataNode; @@ -36,8 +38,10 @@ public interface CpsDataService { * @param dataspaceName dataspace name * @param anchorName anchor name * @param jsonData json data + * @param observedTimestamp observedTimestamp */ - void saveData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String jsonData); + void saveData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String jsonData, + OffsetDateTime observedTimestamp); /** * Persists child data fragment under existing data node for the given anchor and dataspace. @@ -46,21 +50,23 @@ public interface CpsDataService { * @param anchorName anchor name * @param parentNodeXpath parent node xpath * @param jsonData json data + * @param observedTimestamp observedTimestamp */ void saveData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath, - @NonNull String jsonData); + @NonNull String jsonData, OffsetDateTime observedTimestamp); /** - * Persists child data fragment representing list-node (with one or more elements) under existing data node - * for the given anchor and dataspace. + * Persists child data fragment representing list-node (with one or more elements) under existing data node for the + * given anchor and dataspace. * * @param dataspaceName dataspace name * @param anchorName anchor name * @param parentNodeXpath parent node xpath * @param jsonData json data representing list element + * @param observedTimestamp observedTimestamp */ void saveListNodeData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath, - @NonNull String jsonData); + @NonNull String jsonData, OffsetDateTime observedTimestamp); /** * Retrieves datanode by XPath for given dataspace and anchor. @@ -82,9 +88,10 @@ public interface CpsDataService { * @param anchorName anchor name * @param parentNodeXpath xpath to parent node * @param jsonData json data + * @param observedTimestamp observedTimestamp */ void updateNodeLeaves(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath, - @NonNull String jsonData); + @NonNull String jsonData, OffsetDateTime observedTimestamp); /** * Replaces existing data node content including descendants. @@ -93,42 +100,47 @@ public interface CpsDataService { * @param anchorName anchor name * @param parentNodeXpath xpath to parent node * @param jsonData json data + * @param observedTimestamp observedTimestamp */ void replaceNodeTree(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath, - @NonNull String jsonData); + @NonNull String jsonData, OffsetDateTime observedTimestamp); /** - * Replaces (if exists) child data fragment representing list-node (with one or more elements) - * under existing data node for the given anchor and dataspace. + * Replaces (if exists) child data fragment representing list-node (with one or more elements) under existing data + * node for the given anchor and dataspace. * - * @param dataspaceName dataspace name - * @param anchorName anchor name - * @param parentNodeXpath parent node xpath - * @param jsonData json data representing list element + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param parentNodeXpath parent node xpath + * @param jsonData json data representing list element + * @param observedTimestamp observedTimestamp */ void replaceListNodeData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath, - @NonNull String jsonData); + @NonNull String jsonData, OffsetDateTime observedTimestamp); /** - * Deletes (if exists) child data fragment representing list-node (with one or more elements) - * under existing data node for the given anchor and dataspace. + * Deletes (if exists) child data fragment representing list-node (with one or more elements) under existing data + * node for the given anchor and dataspace. * - * @param dataspaceName dataspace name - * @param anchorName anchor name - * @param listNodeXpath list node xpath + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param listNodeXpath list node xpath + * @param observedTimestamp observedTimestamp */ - void deleteListNodeData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String listNodeXpath); + void deleteListNodeData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String listNodeXpath, + OffsetDateTime observedTimestamp); /** - * Updates leaves of DataNode for given dataspace and anchor using xpath, - * along with the leaves of each Child Data Node which already exists. - * This method will throw an exception if data node update or any descendant update does not exist. + * Updates leaves of DataNode for given dataspace and anchor using xpath, along with the leaves of each Child Data + * Node which already exists. This method will throw an exception if data node update or any descendant update does + * not exist. * - * @param dataspaceName dataspace name - * @param anchorName anchor name - * @param parentNodeXpath xpath + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param parentNodeXpath xpath * @param dataNodeUpdatesAsJson json data representing data node updates + * @param observedTimestamp observedTimestamp */ void updateNodeLeavesAndExistingDescendantLeaves(String dataspaceName, String anchorName, String parentNodeXpath, - String dataNodeUpdatesAsJson); + String dataNodeUpdatesAsJson, OffsetDateTime observedTimestamp); } diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java index 8989dc80ef..7b3567ed3c 100755 --- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java @@ -22,6 +22,7 @@ package org.onap.cps.api.impl; +import java.time.OffsetDateTime; import java.util.Collection; import lombok.extern.slf4j.Slf4j; import org.onap.cps.api.CpsAdminService; @@ -61,27 +62,28 @@ public class CpsDataServiceImpl implements CpsDataService { private NotificationService notificationService; @Override - public void saveData(final String dataspaceName, final String anchorName, final String jsonData) { + public void saveData(final String dataspaceName, final String anchorName, final String jsonData, + final OffsetDateTime observedTimestamp) { final var dataNode = buildDataNodeFromJson(dataspaceName, anchorName, ROOT_NODE_XPATH, jsonData); cpsDataPersistenceService.storeDataNode(dataspaceName, anchorName, dataNode); - processDataUpdatedEventAsync(dataspaceName, anchorName); + processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp); } @Override public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath, - final String jsonData) { + final String jsonData, final OffsetDateTime observedTimestamp) { final var dataNode = buildDataNodeFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData); cpsDataPersistenceService.addChildDataNode(dataspaceName, anchorName, parentNodeXpath, dataNode); - processDataUpdatedEventAsync(dataspaceName, anchorName); + processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp); } @Override public void saveListNodeData(final String dataspaceName, final String anchorName, - final String parentNodeXpath, final String jsonData) { + final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) { final Collection dataNodesCollection = buildDataNodeCollectionFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData); cpsDataPersistenceService.addListDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodesCollection); - processDataUpdatedEventAsync(dataspaceName, anchorName); + processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp); } @Override @@ -92,46 +94,48 @@ public class CpsDataServiceImpl implements CpsDataService { @Override public void updateNodeLeaves(final String dataspaceName, final String anchorName, final String parentNodeXpath, - final String jsonData) { + final String jsonData, final OffsetDateTime observedTimestamp) { final var dataNode = buildDataNodeFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData); cpsDataPersistenceService .updateDataLeaves(dataspaceName, anchorName, dataNode.getXpath(), dataNode.getLeaves()); - processDataUpdatedEventAsync(dataspaceName, anchorName); + processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp); } @Override public void updateNodeLeavesAndExistingDescendantLeaves(final String dataspaceName, final String anchorName, - final String parentNodeXpath, - final String dataNodeUpdatesAsJson) { + final String parentNodeXpath, + final String dataNodeUpdatesAsJson, + final OffsetDateTime observedTimestamp) { final Collection dataNodeUpdates = buildDataNodeCollectionFromJson(dataspaceName, anchorName, parentNodeXpath, dataNodeUpdatesAsJson); for (final DataNode dataNodeUpdate : dataNodeUpdates) { processDataNodeUpdate(dataspaceName, anchorName, dataNodeUpdate); } - notificationService.processDataUpdatedEvent(dataspaceName, anchorName); + processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp); } @Override public void replaceNodeTree(final String dataspaceName, final String anchorName, final String parentNodeXpath, - final String jsonData) { + final String jsonData, final OffsetDateTime observedTimestamp) { final var dataNode = buildDataNodeFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData); cpsDataPersistenceService.replaceDataNodeTree(dataspaceName, anchorName, dataNode); - processDataUpdatedEventAsync(dataspaceName, anchorName); + processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp); } @Override public void replaceListNodeData(final String dataspaceName, final String anchorName, final String parentNodeXpath, - final String jsonData) { + final String jsonData, final OffsetDateTime observedTimestamp) { final Collection dataNodes = buildDataNodeCollectionFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData); cpsDataPersistenceService.replaceListDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes); - processDataUpdatedEventAsync(dataspaceName, anchorName); + processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp); } @Override - public void deleteListNodeData(final String dataspaceName, final String anchorName, final String listNodeXpath) { + public void deleteListNodeData(final String dataspaceName, final String anchorName, final String listNodeXpath, + final OffsetDateTime observedTimestamp) { cpsDataPersistenceService.deleteListDataNodes(dataspaceName, anchorName, listNodeXpath); - processDataUpdatedEventAsync(dataspaceName, anchorName); + processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp); } @@ -171,9 +175,10 @@ public class CpsDataServiceImpl implements CpsDataService { } - private void processDataUpdatedEventAsync(final String dataspaceName, final String anchorName) { + private void processDataUpdatedEventAsync(final String dataspaceName, final String anchorName, + final OffsetDateTime observedTimestamp) { try { - notificationService.processDataUpdatedEvent(dataspaceName, anchorName); + notificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp); } catch (final Exception exception) { log.error("Failed to send message to notification service", exception); } diff --git a/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java b/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java index e0c8fe7055..85e5abab0d 100644 --- a/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java +++ b/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java @@ -42,7 +42,7 @@ public class CpsDataUpdatedEventFactory { private static final URI EVENT_SCHEMA; private static final URI EVENT_SOURCE; private static final String EVENT_TYPE = "org.onap.cps.data-updated-event"; - private static final DateTimeFormatter dateTimeFormatter = + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); static { @@ -64,22 +64,25 @@ public class CpsDataUpdatedEventFactory { } /** - * Generates CPS Data Updated event. + * Generates CPS Data Updated event. If observedTimestamp is not provided, then current timestamp is used. * - * @param dataspaceName dataspaceName - * @param anchorName anchorName + * @param dataspaceName dataspaceName + * @param anchorName anchorName + * @param observedTimestamp observedTimestamp * @return CpsDataUpdatedEvent */ - public CpsDataUpdatedEvent createCpsDataUpdatedEvent(final String dataspaceName, final String anchorName) { + public CpsDataUpdatedEvent createCpsDataUpdatedEvent(final String dataspaceName, final String anchorName, + final OffsetDateTime observedTimestamp) { final var dataNode = cpsDataService .getDataNode(dataspaceName, anchorName, "/", FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS); final var anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); - return toCpsDataUpdatedEvent(anchor, dataNode); + return toCpsDataUpdatedEvent(anchor, dataNode, observedTimestamp); } - private CpsDataUpdatedEvent toCpsDataUpdatedEvent(final Anchor anchor, final DataNode dataNode) { + private CpsDataUpdatedEvent toCpsDataUpdatedEvent(final Anchor anchor, final DataNode dataNode, + final OffsetDateTime observedTimestamp) { final var cpsDataUpdatedEvent = new CpsDataUpdatedEvent(); - cpsDataUpdatedEvent.withContent(createContent(anchor, dataNode)); + cpsDataUpdatedEvent.withContent(createContent(anchor, dataNode, observedTimestamp)); cpsDataUpdatedEvent.withId(UUID.randomUUID().toString()); cpsDataUpdatedEvent.withSchema(EVENT_SCHEMA); cpsDataUpdatedEvent.withSource(EVENT_SOURCE); @@ -93,13 +96,15 @@ public class CpsDataUpdatedEventFactory { return data; } - private Content createContent(final Anchor anchor, final DataNode dataNode) { + private Content createContent(final Anchor anchor, final DataNode dataNode, + final OffsetDateTime observedTimestamp) { final var content = new Content(); content.withAnchorName(anchor.getName()); content.withDataspaceName(anchor.getDataspaceName()); content.withSchemaSetName(anchor.getSchemaSetName()); content.withData(createData(dataNode)); - content.withObservedTimestamp(dateTimeFormatter.format(OffsetDateTime.now())); + content.withObservedTimestamp( + DATE_TIME_FORMATTER.format(observedTimestamp == null ? OffsetDateTime.now() : observedTimestamp)); return content; } } diff --git a/cps-service/src/main/java/org/onap/cps/notification/NotificationService.java b/cps-service/src/main/java/org/onap/cps/notification/NotificationService.java index 4745739a4f..029efbe795 100644 --- a/cps-service/src/main/java/org/onap/cps/notification/NotificationService.java +++ b/cps-service/src/main/java/org/onap/cps/notification/NotificationService.java @@ -20,6 +20,7 @@ package org.onap.cps.notification; +import java.time.OffsetDateTime; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -79,15 +80,17 @@ public class NotificationService { * * @param dataspaceName dataspace name * @param anchorName anchor name + * @param observedTimestamp observedTimestamp * @return future */ @Async("notificationExecutor") - public Future processDataUpdatedEvent(final String dataspaceName, final String anchorName) { + public Future processDataUpdatedEvent(final String dataspaceName, final String anchorName, + final OffsetDateTime observedTimestamp) { log.debug("process data updated event for dataspace '{}' & anchor '{}'", dataspaceName, anchorName); try { if (shouldSendNotification(dataspaceName)) { final var cpsDataUpdatedEvent = - cpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(dataspaceName, anchorName); + cpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp); log.debug("data updated event to be published {}", cpsDataUpdatedEvent); notificationPublisher.sendNotification(cpsDataUpdatedEvent); } diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy index 97eac5aaa9..6a0a4649a6 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy @@ -22,6 +22,7 @@ package org.onap.cps.api.impl +import java.time.OffsetDateTime import org.onap.cps.TestUtils import org.onap.cps.api.CpsAdminService import org.onap.cps.api.CpsModuleService @@ -55,18 +56,19 @@ class CpsDataServiceImplSpec extends Specification { def dataspaceName = 'some dataspace' def anchorName = 'some anchor' def schemaSetName = 'some schema set' + def observedTimestamp = OffsetDateTime.now() def 'Saving json data.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') when: 'save data method is invoked with test-tree json data' def jsonData = TestUtils.getResourceFileContent('test-tree.json') - objectUnderTest.saveData(dataspaceName, anchorName, jsonData) + objectUnderTest.saveData(dataspaceName, anchorName, jsonData, observedTimestamp) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.storeDataNode(dataspaceName, anchorName, - { dataNode -> dataNode.xpath == '/test-tree' }) + { dataNode -> dataNode.xpath == '/test-tree' }) and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName) + 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp) } def 'Saving child data fragment under existing node.'() { @@ -74,12 +76,12 @@ class CpsDataServiceImplSpec extends Specification { setupSchemaSetMocks('test-tree.yang') when: 'save data method is invoked with test-tree json data' def jsonData = '{"branch": [{"name": "New"}]}' - objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData) + objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.addChildDataNode(dataspaceName, anchorName, '/test-tree', - { dataNode -> dataNode.xpath == '/test-tree/branch[@name=\'New\']' }) + { dataNode -> dataNode.xpath == '/test-tree/branch[@name=\'New\']' }) and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName) + 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp) } def 'Saving list-node data fragment under existing node.'() { @@ -87,19 +89,19 @@ class CpsDataServiceImplSpec extends Specification { setupSchemaSetMocks('test-tree.yang') when: 'save data method is invoked with list-node json data' def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}' - objectUnderTest.saveListNodeData(dataspaceName, anchorName, '/test-tree', jsonData) + objectUnderTest.saveListNodeData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.addListDataNodes(dataspaceName, anchorName, '/test-tree', - { dataNodeCollection -> - { - assert dataNodeCollection.size() == 2 - assert dataNodeCollection.collect { it.getXpath() } - .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']']) - } + { dataNodeCollection -> + { + assert dataNodeCollection.size() == 2 + assert dataNodeCollection.collect { it.getXpath() } + .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']']) } + } ) and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName) + 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp) } def 'Saving empty list-node data fragment.'() { @@ -107,7 +109,7 @@ class CpsDataServiceImplSpec extends Specification { setupSchemaSetMocks('test-tree.yang') when: 'save data method is invoked with empty list-node data fragment' def jsonData = '{"branch": []}' - objectUnderTest.saveListNodeData(dataspaceName, anchorName, '/test-tree', jsonData) + objectUnderTest.saveListNodeData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp) then: 'invalid data exception is thrown' thrown(DataValidationException) } @@ -127,11 +129,11 @@ class CpsDataServiceImplSpec extends Specification { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') when: 'update data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath' - objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData) + objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.updateDataLeaves(dataspaceName, anchorName, expectedNodeXpath, leaves) and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName) + 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp) where: 'following parameters were used' scenario | parentNodeXpath | jsonData || expectedNodeXpath | leaves 'top level node' | '/' | '{"test-tree": {"branch": []}}' || '/test-tree' | Collections.emptyMap() @@ -142,7 +144,8 @@ class CpsDataServiceImplSpec extends Specification { given: 'schema set for given anchor and dataspace references bookstore model' setupSchemaSetMocks('bookstore.yang') when: 'update data method is invoked with json data #jsonData and parent node xpath' - objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/bookstore/categories[@code=2]', jsonData) + objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/bookstore/categories[@code=2]', + jsonData, observedTimestamp) then: 'the persistence service method is invoked with correct parameters' thrown(DataValidationException) where: 'following parameters were used' @@ -157,23 +160,25 @@ class CpsDataServiceImplSpec extends Specification { and: 'the expected json string' def jsonData = '{"cm-handles":[{"id":"cmHandle001", "additional-properties":[{"name":"P1"}]}]}' when: 'update data method is invoked with json data and parent node xpath' - objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName, '/dmi-registry', jsonData) + objectUnderTest.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName, + '/dmi-registry', jsonData, observedTimestamp) then: 'the persistence service method is invoked with correct parameters' - 1 * mockCpsDataPersistenceService.updateDataLeaves(dataspaceName, anchorName, "/dmi-registry/cm-handles[@id='cmHandle001']", ['id': 'cmHandle001']) + 1 * mockCpsDataPersistenceService.updateDataLeaves(dataspaceName, anchorName, + "/dmi-registry/cm-handles[@id='cmHandle001']", ['id': 'cmHandle001']) and: 'the data updated event is sent to the notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName) + 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp) } def 'Replace data node: #scenario.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') when: 'replace data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath' - objectUnderTest.replaceNodeTree(dataspaceName, anchorName, parentNodeXpath, jsonData) + objectUnderTest.replaceNodeTree(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.replaceDataNodeTree(dataspaceName, anchorName, - { dataNode -> dataNode.xpath == expectedNodeXpath }) + { dataNode -> dataNode.xpath == expectedNodeXpath }) and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName) + 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp) where: 'following parameters were used' scenario | parentNodeXpath | jsonData || expectedNodeXpath 'top level node' | '/' | '{"test-tree": {"branch": []}}' || '/test-tree' @@ -185,19 +190,19 @@ class CpsDataServiceImplSpec extends Specification { setupSchemaSetMocks('test-tree.yang') when: 'replace list data method is invoked with list-node json data' def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}' - objectUnderTest.replaceListNodeData(dataspaceName, anchorName, '/test-tree', jsonData) + objectUnderTest.replaceListNodeData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.replaceListDataNodes(dataspaceName, anchorName, '/test-tree', - { dataNodeCollection -> - { - assert dataNodeCollection.size() == 2 - assert dataNodeCollection.collect { it.getXpath() } - .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']']) - } + { dataNodeCollection -> + { + assert dataNodeCollection.size() == 2 + assert dataNodeCollection.collect { it.getXpath() } + .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']']) } + } ) and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName) + 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp) } def 'Replace with empty list-node data fragment.'() { @@ -205,7 +210,7 @@ class CpsDataServiceImplSpec extends Specification { setupSchemaSetMocks('test-tree.yang') when: 'replace list data method is invoked with empty list-node data fragment' def jsonData = '{"branch": []}' - objectUnderTest.replaceListNodeData(dataspaceName, anchorName, '/test-tree', jsonData) + objectUnderTest.replaceListNodeData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp) then: 'invalid data exception is thrown' thrown(DataValidationException) } @@ -214,11 +219,11 @@ class CpsDataServiceImplSpec extends Specification { 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-node json data' - objectUnderTest.deleteListNodeData(dataspaceName, anchorName, '/test-tree/branch') + objectUnderTest.deleteListNodeData(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.deleteListDataNodes(dataspaceName, anchorName, '/test-tree/branch') and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName) + 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp) } def setupSchemaSetMocks(String... yangResources) { diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy index fec8b7fa79..eefa86e903 100755 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy @@ -22,6 +22,7 @@ package org.onap.cps.api.impl +import java.time.OffsetDateTime import org.onap.cps.TestUtils import org.onap.cps.api.CpsAdminService import org.onap.cps.notification.NotificationService @@ -44,6 +45,7 @@ class E2ENetworkSliceSpec extends Specification { def dataspaceName = 'someDataspace' def anchorName = 'someAnchor' def schemaSetName = 'someSchemaSet' + def noTimestamp = null def setup() { cpsDataServiceImpl.cpsDataPersistenceService = mockDataStoreService @@ -92,7 +94,7 @@ class E2ENetworkSliceSpec extends Specification { YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap) mockModuleStoreService.getYangSchemaResources(dataspaceName, schemaSetName) >> schemaContext when: 'saveData method is invoked' - cpsDataServiceImpl.saveData(dataspaceName, anchorName, jsonData) + cpsDataServiceImpl.saveData(dataspaceName, anchorName, jsonData, noTimestamp) then: 'Parameters are validated and processing is delegated to persistence service' 1 * mockDataStoreService.storeDataNode('someDataspace', 'someAnchor', _) >> { args -> dataNodeStored = args[2]} @@ -124,7 +126,7 @@ class E2ENetworkSliceSpec extends Specification { mockYangTextSchemaSourceSetCache.get('someDataspace', 'someSchemaSet') >> YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap) mockModuleStoreService.getYangSchemaResources('someDataspace', 'someSchemaSet') >> schemaContext when: 'saveData method is invoked' - cpsDataServiceImpl.saveData('someDataspace', 'someAnchor', jsonData) + cpsDataServiceImpl.saveData('someDataspace', 'someAnchor', jsonData, noTimestamp) then: 'parameters are validated and processing is delegated to persistence service' 1 * mockDataStoreService.storeDataNode('someDataspace', 'someAnchor', _) >> { args -> dataNodeStored = args[2]} diff --git a/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy b/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy index 2ce77bd1a8..aa0c7c0b39 100644 --- a/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy @@ -1,12 +1,13 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021 Bell Canada. All rights reserved. + * Copyright (c) 2021 Bell Canada. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,6 +20,9 @@ package org.onap.cps.notification +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter +import org.onap.cps.utils.DateTimeUtility import org.onap.cps.api.CpsAdminService import org.onap.cps.api.CpsDataService import org.onap.cps.event.model.Data @@ -28,8 +32,6 @@ import org.onap.cps.spi.model.DataNodeBuilder import org.springframework.util.StringUtils import spock.lang.Specification -import java.time.format.DateTimeFormatter - class CpsDataUpdateEventFactorySpec extends Specification { def mockCpsDataService = Mock(CpsDataService) @@ -42,7 +44,7 @@ class CpsDataUpdateEventFactorySpec extends Specification { def mySchemasetName = 'my-schemaset-name' def dateTimeFormat = 'yyyy-MM-dd\'T\'HH:mm:ss.SSSZ' - def 'Create a CPS data updated event successfully.'() { + def 'Create a CPS data updated event successfully: #scenario'() { given: 'cps admin service is able to return anchor details' mockCpsAdminService.getAnchor(myDataspaceName, myAnchorName) >> @@ -51,12 +53,14 @@ class CpsDataUpdateEventFactorySpec extends Specification { def xpath = '/' def dataNode = new DataNodeBuilder().withXpath(xpath).withLeaves(['leafName': 'leafValue']).build() mockCpsDataService.getDataNode( - myDataspaceName, myAnchorName, xpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode + myDataspaceName, myAnchorName, xpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode when: 'CPS data updated event is created' - def cpsDataUpdatedEvent = objectUnderTest.createCpsDataUpdatedEvent(myDataspaceName, myAnchorName) + def cpsDataUpdatedEvent = objectUnderTest.createCpsDataUpdatedEvent(myDataspaceName, + myAnchorName, DateTimeUtility.toOffsetDateTime(inputObservedTimestamp)) + + then: 'CPS data updated event is created with correct envelope' - then: 'CPS data updated event is created with expected values' with(cpsDataUpdatedEvent) { type == 'org.onap.cps.data-updated-event' source == new URI('urn:cps:org.onap.cps') @@ -64,13 +68,24 @@ class CpsDataUpdateEventFactorySpec extends Specification { StringUtils.hasText(id) content != null } + and: 'correct content' with(cpsDataUpdatedEvent.content) { assert isExpectedDateTimeFormat(observedTimestamp): "$observedTimestamp is not in $dateTimeFormat format" - anchorName == myAnchorName - dataspaceName == myDataspaceName - schemaSetName == mySchemasetName - data == new Data().withAdditionalProperty('leafName', 'leafValue') + if (inputObservedTimestamp != null) + assert observedTimestamp == inputObservedTimestamp + else + assert OffsetDateTime.now().minusSeconds(20).isBefore( + DateTimeUtility.toOffsetDateTime(observedTimestamp)) + assert anchorName == myAnchorName + assert dataspaceName == myDataspaceName + assert schemaSetName == mySchemasetName + assert data == new Data().withAdditionalProperty('leafName', 'leafValue') } + where: + scenario | inputObservedTimestamp + 'with observed timestamp -0400' | '2021-01-01T23:00:00.345-0400' + 'with observed timestamp +0400' | '2021-01-01T23:00:00.345+0400' + 'missing observed timestamp' | null } def isExpectedDateTimeFormat(String observedTimestamp) { diff --git a/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy index ab727671e1..875113d225 100644 --- a/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy @@ -20,6 +20,7 @@ package org.onap.cps.notification +import java.time.OffsetDateTime import org.onap.cps.config.AsyncConfig import org.onap.cps.event.model.CpsDataUpdatedEvent import org.spockframework.spring.SpringBean @@ -55,12 +56,13 @@ class NotificationServiceSpec extends Specification { @Shared def myDataspacePublishedName = 'my-dataspace-published' def myAnchorName = 'my-anchorname' + def myObservedTimestamp = OffsetDateTime.now() def 'Skip sending notification when disabled.'() { given: 'notification is disabled' spyNotificationProperties.isEnabled() >> false when: 'dataUpdatedEvent is received' - objectUnderTest.processDataUpdatedEvent(myDataspacePublishedName, myAnchorName) + objectUnderTest.processDataUpdatedEvent(myDataspacePublishedName, myAnchorName, myObservedTimestamp) then: 'the notification is not sent' 0 * mockNotificationPublisher.sendNotification(_) } @@ -70,10 +72,11 @@ class NotificationServiceSpec extends Specification { spyNotificationProperties.isEnabled() >> true and: 'event factory can create event successfully' def cpsDataUpdatedEvent = new CpsDataUpdatedEvent() - mockCpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(dataspaceName, myAnchorName) >> cpsDataUpdatedEvent + mockCpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(dataspaceName, myAnchorName, myObservedTimestamp) >> + cpsDataUpdatedEvent when: 'dataUpdatedEvent is received' - def future = objectUnderTest.processDataUpdatedEvent(dataspaceName, myAnchorName) - and: 'wait for async processing is completed' + def future = objectUnderTest.processDataUpdatedEvent(dataspaceName, myAnchorName, myObservedTimestamp) + and: 'wait for async processing to complete' future.get(10, TimeUnit.SECONDS) then: 'async process completed successfully' future.isDone() @@ -89,11 +92,11 @@ class NotificationServiceSpec extends Specification { given: 'notification is enabled' spyNotificationProperties.isEnabled() >> true and: 'event factory can not create event successfully' - mockCpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(myDataspacePublishedName, myAnchorName) >> + mockCpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(myDataspacePublishedName, myAnchorName, myObservedTimestamp) >> { throw new Exception("Could not create event") } when: 'event is sent for processing' - def future = objectUnderTest.processDataUpdatedEvent(myDataspacePublishedName, myAnchorName) - and: 'wait for async processing is completed' + def future = objectUnderTest.processDataUpdatedEvent(myDataspacePublishedName, myAnchorName, myObservedTimestamp) + and: 'wait for async processing to complete' future.get(10, TimeUnit.SECONDS) then: 'async process completed successfully' future.isDone() diff --git a/cps-service/src/test/java/org/onap/cps/utils/DateTimeUtility.java b/cps-service/src/test/java/org/onap/cps/utils/DateTimeUtility.java new file mode 100644 index 0000000000..f8d709647c --- /dev/null +++ b/cps-service/src/test/java/org/onap/cps/utils/DateTimeUtility.java @@ -0,0 +1,40 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (c) 2021 Bell Canada. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.utils; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import org.springframework.util.StringUtils; + +public interface DateTimeUtility { + + String ISO_TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + DateTimeFormatter ISO_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern(ISO_TIMESTAMP_PATTERN); + + static OffsetDateTime toOffsetDateTime(String datetTimestampAsString) { + return ! StringUtils.hasLength(datetTimestampAsString) + ? null : OffsetDateTime.parse(datetTimestampAsString, ISO_TIMESTAMP_FORMATTER); + } + + static String toString(OffsetDateTime offsetDateTime) { + return offsetDateTime != null ? ISO_TIMESTAMP_FORMATTER.format(offsetDateTime) : null; + } +} -- cgit 1.2.3-korg