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 <aditya.puthuparambil@bell.ca>
Change-Id: I1f92da3da7b3a13c45405fdf44e5fef861991d9a
Signed-off-by: Renu Kumari <renu.kumari@bell.ca>
diff --git a/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java b/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
index 5c79472..0e2050e 100755
--- a/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
+++ b/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
@@ -1,6 +1,6 @@
/*
* ============LICENSE_START=======================================================
- * Copyright (C) 2020 Bell Canada.
+ * Copyright (C) 2020-2021 Bell Canada.
* Modifications Copyright (C) 2021 Pantheon.tech
* Modifications Copyright (C) 2021 Nordix Foundation
* ================================================================================
@@ -9,6 +9,7 @@
* 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.
@@ -21,8 +22,10 @@
package org.onap.cps.rest.controller;
-import javax.validation.Valid;
-import javax.validation.constraints.NotNull;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import javax.validation.ValidationException;
+import org.apache.commons.lang3.StringUtils;
import org.onap.cps.api.CpsDataService;
import org.onap.cps.rest.api.CpsDataApi;
import org.onap.cps.spi.FetchDescendantsOption;
@@ -38,25 +41,29 @@
public class DataRestController implements CpsDataApi {
private static final String ROOT_XPATH = "/";
+ private static final String ISO_TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
+ private static final DateTimeFormatter ISO_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern(ISO_TIMESTAMP_FORMAT);
@Autowired
private CpsDataService cpsDataService;
@Override
public ResponseEntity<String> createNode(final String dataspaceName, final String anchorName,
- final String jsonData, final String parentNodeXpath) {
+ final String jsonData, final String parentNodeXpath, final String observedTimestamp) {
if (isRootXpath(parentNodeXpath)) {
- cpsDataService.saveData(dataspaceName, anchorName, jsonData);
+ cpsDataService.saveData(dataspaceName, anchorName, jsonData, toOffsetDateTime(observedTimestamp));
} else {
- cpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, jsonData);
+ cpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, jsonData,
+ toOffsetDateTime(observedTimestamp));
}
return new ResponseEntity<>(HttpStatus.CREATED);
}
@Override
public ResponseEntity<String> addListNodeElements(final String parentNodeXpath,
- final String dataspaceName, final String anchorName, final String jsonData) {
- cpsDataService.saveListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData);
+ final String dataspaceName, final String anchorName, final String jsonData, final String observedTimestamp) {
+ cpsDataService.saveListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData,
+ toOffsetDateTime(observedTimestamp));
return new ResponseEntity<>(HttpStatus.CREATED);
}
@@ -77,33 +84,48 @@
@Override
public ResponseEntity<Object> updateNodeLeaves(final String dataspaceName,
- final String anchorName, final String jsonData, final String parentNodeXpath) {
- cpsDataService.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData);
+ final String anchorName, final String jsonData, final String parentNodeXpath, final String observedTimestamp) {
+ cpsDataService.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonData,
+ toOffsetDateTime(observedTimestamp));
return new ResponseEntity<>(HttpStatus.OK);
}
@Override
- public ResponseEntity<Object> replaceNode(final String dataspaceName,
- final String anchorName, @Valid final String jsonData, @Valid final String parentNodeXpath) {
- cpsDataService.replaceNodeTree(dataspaceName, anchorName, parentNodeXpath, jsonData);
+ public ResponseEntity<Object> replaceNode(final String dataspaceName, final String anchorName,
+ final String jsonData, final String parentNodeXpath, final String observedTimestamp) {
+ cpsDataService
+ .replaceNodeTree(dataspaceName, anchorName, parentNodeXpath, jsonData, toOffsetDateTime(observedTimestamp));
return new ResponseEntity<>(HttpStatus.OK);
}
@Override
- public ResponseEntity<String> replaceListNodeElements(@NotNull @Valid final String parentNodeXpath,
- final String dataspaceName, final String anchorName, @Valid final String jsonData) {
- cpsDataService.replaceListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData);
+ public ResponseEntity<String> replaceListNodeElements(final String parentNodeXpath,
+ final String dataspaceName, final String anchorName, final String jsonData,
+ final String observedTimestamp) {
+ cpsDataService.replaceListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData,
+ toOffsetDateTime(observedTimestamp));
return new ResponseEntity<>(HttpStatus.OK);
}
@Override
public ResponseEntity<Void> deleteListNodeElements(final String dataspaceName, final String anchorName,
- final String listNodeXpath) {
- cpsDataService.deleteListNodeData(dataspaceName, anchorName, listNodeXpath);
+ final String listNodeXpath, final String observedTimestamp) {
+ cpsDataService
+ .deleteListNodeData(dataspaceName, anchorName, listNodeXpath, toOffsetDateTime(observedTimestamp));
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
private static boolean isRootXpath(final String xpath) {
return ROOT_XPATH.equals(xpath);
}
+
+ private OffsetDateTime toOffsetDateTime(final String datetTimestamp) {
+ try {
+ return StringUtils.isEmpty(datetTimestamp)
+ ? null : OffsetDateTime.parse(datetTimestamp, ISO_TIMESTAMP_FORMATTER);
+ } catch (final Exception exception) {
+ throw new ValidationException(
+ String.format("observed-timestamp must be in '%s' format", ISO_TIMESTAMP_FORMAT));
+ }
+ }
}
diff --git a/cps-rest/src/main/java/org/onap/cps/rest/exceptions/CpsRestExceptionHandler.java b/cps-rest/src/main/java/org/onap/cps/rest/exceptions/CpsRestExceptionHandler.java
index 143ad8b..d790e08 100755
--- a/cps-rest/src/main/java/org/onap/cps/rest/exceptions/CpsRestExceptionHandler.java
+++ b/cps-rest/src/main/java/org/onap/cps/rest/exceptions/CpsRestExceptionHandler.java
@@ -22,6 +22,7 @@
package org.onap.cps.rest.exceptions;
import javax.servlet.http.HttpServletRequest;
+import javax.validation.ValidationException;
import lombok.extern.slf4j.Slf4j;
import org.onap.cps.rest.controller.AdminRestController;
import org.onap.cps.rest.controller.DataRestController;
@@ -67,6 +68,11 @@
return buildErrorResponse(HttpStatus.BAD_REQUEST, exception);
}
+ @ExceptionHandler({ValidationException.class})
+ public static ResponseEntity<Object> handleBadRequestExceptions(final ValidationException validationException) {
+ return buildErrorResponse(HttpStatus.BAD_REQUEST, validationException);
+ }
+
@ExceptionHandler({NotFoundInDataspaceException.class, DataNodeNotFoundException.class})
public static ResponseEntity<Object> handleNotFoundExceptions(final CpsException exception,
final HttpServletRequest request) {
diff --git a/cps-rest/src/main/resources/static/components.yml b/cps-rest/src/main/resources/static/components.yml
index 51a49a6..75a6f99 100644
--- a/cps-rest/src/main/resources/static/components.yml
+++ b/cps-rest/src/main/resources/static/components.yml
@@ -158,6 +158,14 @@
schema:
type: boolean
default: false
+ observedTimestampInQuery:
+ name: observed-timestamp
+ in: query
+ description: observed-timestamp
+ required: false
+ schema:
+ type: string
+ example: '2021-03-21T00:10:34.030-0100'
responses:
NotFound:
diff --git a/cps-rest/src/main/resources/static/cpsData.yml b/cps-rest/src/main/resources/static/cpsData.yml
index 9c4f333..75d9544 100644
--- a/cps-rest/src/main/resources/static/cpsData.yml
+++ b/cps-rest/src/main/resources/static/cpsData.yml
@@ -55,6 +55,7 @@
- $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
- $ref: 'components.yml#/components/parameters/anchorNameInPath'
- $ref: 'components.yml#/components/parameters/requiredXpathInQuery'
+ - $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
requestBody:
required: true
content:
@@ -81,6 +82,7 @@
- $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
- $ref: 'components.yml#/components/parameters/anchorNameInPath'
- $ref: 'components.yml#/components/parameters/requiredXpathInQuery'
+ - $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
requestBody:
required: true
content:
@@ -107,6 +109,7 @@
- $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
- $ref: 'components.yml#/components/parameters/anchorNameInPath'
- $ref: 'components.yml#/components/parameters/requiredXpathInQuery'
+ - $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
responses:
'204':
$ref: 'components.yml#/components/responses/NoContent'
@@ -128,6 +131,7 @@
- $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
- $ref: 'components.yml#/components/parameters/anchorNameInPath'
- $ref: 'components.yml#/components/parameters/xpathInQuery'
+ - $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
requestBody:
required: true
content:
@@ -154,6 +158,7 @@
- $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
- $ref: 'components.yml#/components/parameters/anchorNameInPath'
- $ref: 'components.yml#/components/parameters/xpathInQuery'
+ - $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
requestBody:
required: true
content:
@@ -180,6 +185,7 @@
- $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
- $ref: 'components.yml#/components/parameters/anchorNameInPath'
- $ref: 'components.yml#/components/parameters/xpathInQuery'
+ - $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
requestBody:
required: true
content:
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
index d3d42e3..1d51ec4 100755
--- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
+++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
@@ -30,13 +30,10 @@
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
-import org.modelmapper.ModelMapper
-import org.onap.cps.api.CpsAdminService
import org.onap.cps.api.CpsDataService
-import org.onap.cps.api.CpsModuleService
-import org.onap.cps.api.CpsQueryService
import org.onap.cps.spi.model.DataNode
import org.onap.cps.spi.model.DataNodeBuilder
+import org.onap.cps.utils.DateTimeUtility
import org.spockframework.spring.SpringBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
@@ -62,14 +59,15 @@
def dataNodeBaseEndpoint
def dataspaceName = 'my_dataspace'
def anchorName = 'my_anchor'
+ def noTimestamp = null
@Shared
static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/xpath')
- .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
+ .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
@Shared
static DataNode dataNodeWithChild = new DataNodeBuilder().withXpath('/parent')
- .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()
+ .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()
def setup() {
dataNodeBaseEndpoint = "$basePath/v1/dataspaces/$dataspaceName"
@@ -81,22 +79,46 @@
def json = 'some json (this is not validated)'
when: 'post is invoked with datanode endpoint and json'
def response =
- mvc.perform(
- post(endpoint)
- .contentType(MediaType.APPLICATION_JSON)
- .param('xpath', parentNodeXpath)
- .content(json)
- ).andReturn().response
+ mvc.perform(
+ post(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .param('xpath', parentNodeXpath)
+ .content(json)
+ ).andReturn().response
then: 'a created response is returned'
response.status == HttpStatus.CREATED.value()
then: 'the java API was called with the correct parameters'
- 1 * mockCpsDataService.saveData(dataspaceName, anchorName, json)
+ 1 * mockCpsDataService.saveData(dataspaceName, anchorName, json, noTimestamp)
where: 'following xpath parameters are are used'
scenario | parentNodeXpath
'no xpath parameter' | ''
'xpath parameter point root' | '/'
}
+ def 'Create a node with observed-timestamp'() {
+ given: 'some json to create a data node'
+ def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
+ def json = 'some json (this is not validated)'
+ when: 'post is invoked with datanode endpoint and json'
+ def response =
+ mvc.perform(
+ post(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .param('xpath', '')
+ .param('observed-timestamp', observedTimestamp)
+ .content(json)
+ ).andReturn().response
+ then: 'a created response is returned'
+ response.status == expectedHttpStatus.value()
+ then: 'the java API was called with the correct parameters'
+ expectedApiCount * mockCpsDataService.saveData(dataspaceName, anchorName, json,
+ { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+ where:
+ scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
+ 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.CREATED
+ 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
+ }
+
def 'Create a child node'() {
given: 'some json to create a data node'
def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
@@ -104,34 +126,47 @@
and: 'parent node xpath'
def parentNodeXpath = 'some xpath'
when: 'post is invoked with datanode endpoint and json'
+ def postRequestBuilder = post(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .param('xpath', parentNodeXpath)
+ .content(json)
+ if (observedTimestamp != null)
+ postRequestBuilder.param('observed-timestamp', observedTimestamp)
def response =
- mvc.perform(
- post(endpoint)
- .contentType(MediaType.APPLICATION_JSON)
- .param('xpath', parentNodeXpath)
- .content(json)
- ).andReturn().response
+ mvc.perform(postRequestBuilder).andReturn().response
then: 'a created response is returned'
response.status == HttpStatus.CREATED.value()
then: 'the java API was called with the correct parameters'
- 1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, json)
+ 1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, json,
+ DateTimeUtility.toOffsetDateTime(observedTimestamp))
+ where:
+ scenario | observedTimestamp
+ 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400'
+ 'without observed-timestamp' | null
}
- def 'Create list node child elements.'() {
+ def 'Create list node child elements #scenario.'() {
given: 'parent node xpath and json data inputs'
def parentNodeXpath = 'parent node xpath'
def jsonData = 'json data'
when: 'post is invoked list-node endpoint'
- def response = mvc.perform(
- post("$dataNodeBaseEndpoint/anchors/$anchorName/list-node")
- .contentType(MediaType.APPLICATION_JSON)
- .param('xpath', parentNodeXpath)
- .content(jsonData)
- ).andReturn().response
+ def postRequestBuilder = post("$dataNodeBaseEndpoint/anchors/$anchorName/list-node")
+ .contentType(MediaType.APPLICATION_JSON)
+ .param('xpath', parentNodeXpath)
+ .content(jsonData)
+ if (observedTimestamp != null)
+ postRequestBuilder.param('observed-timestamp', observedTimestamp)
+ def response = mvc.perform(postRequestBuilder).andReturn().response
then: 'a created response is returned'
- response.status == HttpStatus.CREATED.value()
+ response.status == expectedHttpStatus.value()
then: 'the java API was called with the correct parameters'
- 1 * mockCpsDataService.saveListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData)
+ expectedApiCount * mockCpsDataService.saveListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData,
+ { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+ where:
+ scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
+ 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.CREATED
+ 'without observed-timestamp' | null || 1 | HttpStatus.CREATED
+ 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
}
def 'Get data node with leaves'() {
@@ -141,8 +176,8 @@
mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> dataNodeWithLeavesNoChildren
when: 'get request is performed through REST API'
def response =
- mvc.perform(get(endpoint).param('xpath', xpath))
- .andReturn().response
+ mvc.perform(get(endpoint).param('xpath', xpath))
+ .andReturn().response
then: 'a success response is returned'
response.status == HttpStatus.OK.value()
and: 'response contains expected leaf and value'
@@ -158,11 +193,11 @@
mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> dataNode
when: 'get request is performed through REST API'
def response =
- mvc.perform(
- get(endpoint)
- .param('xpath', xpath)
- .param('include-descendants', includeDescendantsOption))
- .andReturn().response
+ mvc.perform(
+ get(endpoint)
+ .param('xpath', xpath)
+ .param('include-descendants', includeDescendantsOption))
+ .andReturn().response
then: 'a success response is returned'
response.status == HttpStatus.OK.value()
and: 'the response contains child is #expectChildInResponse'
@@ -180,14 +215,14 @@
def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
when: 'patch request is performed'
def response =
- mvc.perform(
- patch(endpoint)
- .contentType(MediaType.APPLICATION_JSON)
- .content(jsonData)
- .param('xpath', inputXpath)
- ).andReturn().response
+ mvc.perform(
+ patch(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonData)
+ .param('xpath', inputXpath)
+ ).andReturn().response
then: 'the service method is invoked with expected parameters'
- 1 * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, xpathServiceParameter, jsonData)
+ 1 * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, xpathServiceParameter, jsonData, null)
and: 'response status indicates success'
response.status == HttpStatus.OK.value()
where:
@@ -197,20 +232,44 @@
'some xpath by parent' | '/some/xpath' || '/some/xpath'
}
+ def 'Update data node leaves with observedTimestamp'() {
+ given: 'json data'
+ def jsonData = 'json data'
+ def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
+ when: 'patch request is performed'
+ def response =
+ mvc.perform(
+ patch(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonData)
+ .param('xpath', '/')
+ .param('observed-timestamp', observedTimestamp)
+ ).andReturn().response
+ then: 'the service method is invoked with expected parameters'
+ expectedApiCount * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, '/', jsonData,
+ { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+ and: 'response status indicates success'
+ response.status == expectedHttpStatus.value()
+ where:
+ scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
+ 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
+ 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
+ }
+
def 'Replace data node tree: #scenario.'() {
given: 'json data'
def jsonData = 'json data'
def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
when: 'put request is performed'
def response =
- mvc.perform(
- put(endpoint)
- .contentType(MediaType.APPLICATION_JSON)
- .content(jsonData)
- .param('xpath', inputXpath))
- .andReturn().response
+ mvc.perform(
+ put(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonData)
+ .param('xpath', inputXpath))
+ .andReturn().response
then: 'the service method is invoked with expected parameters'
- 1 * mockCpsDataService.replaceNodeTree(dataspaceName, anchorName, xpathServiceParameter, jsonData)
+ 1 * mockCpsDataService.replaceNodeTree(dataspaceName, anchorName, xpathServiceParameter, jsonData, noTimestamp)
and: 'response status indicates success'
response.status == HttpStatus.OK.value()
where:
@@ -220,34 +279,72 @@
'some xpath by parent' | '/some/xpath' || '/some/xpath'
}
+ def 'Replace data node tree with observedTimestamp.'() {
+ given: 'json data'
+ def jsonData = 'json data'
+ def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
+ when: 'put request is performed'
+ def response =
+ mvc.perform(
+ put(endpoint)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonData)
+ .param('xpath', '')
+ .param('observed-timestamp', observedTimestamp))
+ .andReturn().response
+ then: 'the service method is invoked with expected parameters'
+ expectedApiCount * mockCpsDataService.replaceNodeTree(dataspaceName, anchorName, '/', jsonData,
+ { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+ and: 'response status indicates success'
+ response.status == expectedHttpStatus.value()
+ where:
+ scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
+ 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
+ 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
+ }
+
def 'Replace list node child elements.'() {
given: 'parent node xpath and json data inputs'
def parentNodeXpath = 'parent node xpath'
def jsonData = 'json data'
when: 'patch is invoked list-node endpoint'
- def response = mvc.perform(
- patch("$dataNodeBaseEndpoint/anchors/$anchorName/list-node")
- .contentType(MediaType.APPLICATION_JSON)
- .param('xpath', parentNodeXpath)
- .content(jsonData)
- ).andReturn().response
+ def patchRequestBuilder = patch("$dataNodeBaseEndpoint/anchors/$anchorName/list-node")
+ .contentType(MediaType.APPLICATION_JSON)
+ .param('xpath', parentNodeXpath)
+ .content(jsonData)
+ if (observedTimestamp != null)
+ patchRequestBuilder.param('observed-timestamp', observedTimestamp)
+ def response = mvc.perform(patchRequestBuilder).andReturn().response
then: 'a success response is returned'
- response.status == HttpStatus.OK.value()
- then: 'the java API was called with the correct parameters'
- 1 * mockCpsDataService.replaceListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData)
+ response.status == expectedHttpStatus.value()
+ and: 'the java API was called with the correct parameters'
+ expectedApiCount * mockCpsDataService.replaceListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData,
+ { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+ where:
+ scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
+ 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.OK
+ 'without observed-timestamp' | null || 1 | HttpStatus.OK
+ 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
}
- def 'Delete list node child elements.'() {
+ def 'Delete list node child elements. #scenario'() {
given: 'list node xpath'
def listNodeXpath = 'list node xpath'
when: 'delete is invoked list-node endpoint'
- def response = mvc.perform(
- delete("$dataNodeBaseEndpoint/anchors/$anchorName/list-node")
- .param('xpath', listNodeXpath)
- ).andReturn().response
+ def deleteRequestBuilder = delete("$dataNodeBaseEndpoint/anchors/$anchorName/list-node")
+ .param('xpath', listNodeXpath)
+ if (observedTimestamp != null)
+ deleteRequestBuilder.param('observed-timestamp', observedTimestamp)
+ def response = mvc.perform(deleteRequestBuilder).andReturn().response
then: 'a success response is returned'
- response.status == HttpStatus.NO_CONTENT.value()
- then: 'the java API was called with the correct parameters'
- 1 * mockCpsDataService.deleteListNodeData(dataspaceName, anchorName, listNodeXpath)
+ response.status == expectedHttpStatus.value()
+ and: 'the java API was called with the correct parameters'
+ expectedApiCount * mockCpsDataService.deleteListNodeData(dataspaceName, anchorName, listNodeXpath,
+ { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+ where:
+ scenario | observedTimestamp || expectedApiCount | expectedHttpStatus
+ 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.NO_CONTENT
+ 'without observed-timestamp' | null || 1 | HttpStatus.NO_CONTENT
+ 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
}
}
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
index f44518d..079a59c 100644
--- a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
+++ b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
@@ -111,7 +111,7 @@
def response = performTestRequest()
then: 'an HTTP Not Found response is returned with correct message and details'
assertTestResponse(response, NOT_FOUND, 'Object not found',
- 'Description does not exist in dataspace MyDataSpace.')
+ 'Description does not exist in dataspace MyDataSpace.')
}
def 'Request with an object already defined exception returns HTTP Status Conflict.'() {
@@ -120,8 +120,8 @@
def response = performTestRequest()
then: 'a HTTP conflict response is returned with correct message an details'
assertTestResponse(response, CONFLICT,
- "Already defined exception",
- "Anchor with name ${existingObjectName} already exists for ${dataspaceName}.")
+ "Already defined exception",
+ "Anchor with name ${existingObjectName} already exists for ${dataspaceName}.")
}
def 'Get request with a #exceptionThrown.class.simpleName returns HTTP Status Bad Request'() {
@@ -152,15 +152,16 @@
* NB. This method tests the expected behavior for POST request only;
* testing of PUT and PATCH requests omitted due to same NOT 'GET' condition is being used.
*/
+
def 'Post request with #exceptionThrown.class.simpleName returns HTTP Status Bad Request.'() {
given: '#exception is thrown the service indicating data is not found'
- mockCpsDataService.saveData(_, _, _, _) >> { throw exceptionThrown }
+ mockCpsDataService.saveData(_, _, _, _, _) >> { throw exceptionThrown }
when: 'data update request is performed'
def response = mvc.perform(
- post("$basePath/v1/dataspaces/dataspace-name/anchors/anchor-name/nodes")
- .contentType(MediaType.APPLICATION_JSON)
- .param('xpath', 'parent node xpath')
- .content('json data')
+ post("$basePath/v1/dataspaces/dataspace-name/anchors/anchor-name/nodes")
+ .contentType(MediaType.APPLICATION_JSON)
+ .param('xpath', 'parent node xpath')
+ .content('json data')
).andReturn().response
then: 'response code indicates bad input parameters'
response.status == BAD_REQUEST.value()
@@ -179,8 +180,8 @@
def performTestRequest() {
return mvc.perform(
- get("$basePath/v1/dataspaces/dataspace-name/anchors"))
- .andReturn().response
+ get("$basePath/v1/dataspaces/dataspace-name/anchors"))
+ .andReturn().response
}
static void assertTestResponse(response, expectedStatus, expectedErrorMessage, expectedErrorDetails) {
diff --git a/cps-rest/src/test/java/org/onap/cps/utils/DateTimeUtility.java b/cps-rest/src/test/java/org/onap/cps/utils/DateTimeUtility.java
new file mode 100644
index 0000000..f8d7096
--- /dev/null
+++ b/cps-rest/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;
+ }
+}