diff options
author | aditya puthuparambil <aditya.puthuparambil@bell.ca> | 2021-08-24 17:44:34 +0100 |
---|---|---|
committer | Renu Kumari <renu.kumari@bell.ca> | 2021-08-25 15:01:06 -0400 |
commit | 673c6d94830a1677e685cab82a76747a0808d347 (patch) | |
tree | 7eaaee1002bfda4adc20503b4850e8070fdeffc7 /cps-rest | |
parent | 0b0a89eec95e2cb2671bdd393e94fdaa89b9cc66 (diff) |
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>
Diffstat (limited to 'cps-rest')
7 files changed, 275 insertions, 95 deletions
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 5c79472a4c..0e2050e5cc 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 @@ import org.springframework.web.bind.annotation.RestController; 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 @@ public class DataRestController implements CpsDataApi { @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 143ad8b886..d790e0802d 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 @@ public class CpsRestExceptionHandler { 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 51a49a6e9f..75a6f99fc9 100644 --- a/cps-rest/src/main/resources/static/components.yml +++ b/cps-rest/src/main/resources/static/components.yml @@ -158,6 +158,14 @@ components: 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 9c4f3334e1..75d954473d 100644 --- a/cps-rest/src/main/resources/static/cpsData.yml +++ b/cps-rest/src/main/resources/static/cpsData.yml @@ -55,6 +55,7 @@ listNodeByDataspaceAndAnchor: - $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 @@ listNodeByDataspaceAndAnchor: - $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 @@ listNodeByDataspaceAndAnchor: - $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 @@ nodesByDataspaceAndAnchor: - $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 @@ nodesByDataspaceAndAnchor: - $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 @@ nodesByDataspaceAndAnchor: - $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 d3d42e3065..1d51ec4aca 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.MockMvcRequestBuilder 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 @@ class DataRestControllerSpec extends Specification { 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 @@ class DataRestControllerSpec extends Specification { 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 @@ class DataRestControllerSpec extends Specification { 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 @@ class DataRestControllerSpec extends Specification { 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 @@ class DataRestControllerSpec extends Specification { 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 @@ class DataRestControllerSpec extends Specification { 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 @@ class DataRestControllerSpec extends Specification { '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 @@ class DataRestControllerSpec extends Specification { '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 f44518d4b8..079a59c667 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 @@ class CpsRestExceptionHandlerSpec extends Specification { 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 @@ class CpsRestExceptionHandlerSpec extends Specification { 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 @@ class CpsRestExceptionHandlerSpec extends Specification { * 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 @@ class CpsRestExceptionHandlerSpec extends Specification { 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 0000000000..f8d709647c --- /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; + } +} |