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 --- .../cps/api/impl/CpsDataServiceImplSpec.groovy | 75 ++++++++++++---------- .../onap/cps/api/impl/E2ENetworkSliceSpec.groovy | 6 +- .../CpsDataUpdateEventFactorySpec.groovy | 37 +++++++---- .../notification/NotificationServiceSpec.groovy | 17 +++-- 4 files changed, 80 insertions(+), 55 deletions(-) (limited to 'cps-service/src/test/groovy/org/onap') 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 97eac5aaa..6a0a4649a 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 fec8b7fa7..eefa86e90 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 2ce77bd1a..aa0c7c0b3 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 ab727671e..875113d22 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() -- cgit 1.2.3-korg