From: Bruno Sakoto Date: Fri, 11 Mar 2022 22:32:13 +0000 (+0000) Subject: Merge "Async: NCMP Rest impl. including Request ID generation" X-Git-Tag: 3.0.0~4 X-Git-Url: https://gerrit.onap.org/r/gitweb?p=cps.git;a=commitdiff_plain;h=697caa85dd35d5996d604935987e43b61b5811c2;hp=ee1ca9a51fb57c33814e21785ce98f8b563f44f8 Merge "Async: NCMP Rest impl. including Request ID generation" --- diff --git a/cps-application/src/main/resources/application.yml b/cps-application/src/main/resources/application.yml index 723e2ca19..4dfeee809 100644 --- a/cps-application/src/main/resources/application.yml +++ b/cps-application/src/main/resources/application.yml @@ -135,4 +135,4 @@ dmi: username: ${DMI_USERNAME} password: ${DMI_PASSWORD} api: - base-path: /dmi + base-path: dmi diff --git a/cps-ncmp-rest/docs/openapi/components.yaml b/cps-ncmp-rest/docs/openapi/components.yaml index 6477e34f3..8b02dd10a 100644 --- a/cps-ncmp-rest/docs/openapi/components.yaml +++ b/cps-ncmp-rest/docs/openapi/components.yaml @@ -329,6 +329,18 @@ components: sample 3: value: options: (depth=2,fields=book/authors) + topicParamInQuery: + name: topic + in: query + description: topic parameter in query. + required: false + schema: + type: string + allowReserved: true + examples: + sample 1: + value: + topic: my-topic-name contentParamInHeader: name: Content-Type in: header diff --git a/cps-ncmp-rest/docs/openapi/ncmp.yml b/cps-ncmp-rest/docs/openapi/ncmp.yml index a267fb491..1afd7c911 100755 --- a/cps-ncmp-rest/docs/openapi/ncmp.yml +++ b/cps-ncmp-rest/docs/openapi/ncmp.yml @@ -29,6 +29,7 @@ getResourceDataForPassthroughOperational: - $ref: 'components.yaml#/components/parameters/resourceIdentifierInQuery' - $ref: 'components.yaml#/components/parameters/acceptParamInHeader' - $ref: 'components.yaml#/components/parameters/optionsParamInQuery' + - $ref: 'components.yaml#/components/parameters/topicParamInQuery' responses: 200: description: OK @@ -60,6 +61,7 @@ resourceDataForPassthroughRunning: - $ref: 'components.yaml#/components/parameters/resourceIdentifierInQuery' - $ref: 'components.yaml#/components/parameters/acceptParamInHeader' - $ref: 'components.yaml#/components/parameters/optionsParamInQuery' + - $ref: 'components.yaml#/components/parameters/topicParamInQuery' responses: 200: description: OK diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java index 2a336d52b..b5c8d14a9 100755 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java @@ -76,17 +76,20 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { * @param resourceIdentifier resource identifier * @param acceptParamInHeader accept header parameter * @param optionsParamInQuery options query parameter + * @param topicParamInQuery topic query parameter * @return {@code ResponseEntity} response from dmi plugin */ @Override public ResponseEntity getResourceDataOperationalForCmHandle(final String cmHandle, final @NotNull @Valid String resourceIdentifier, final String acceptParamInHeader, - final @Valid String optionsParamInQuery) { + final @Valid String optionsParamInQuery, + final @Valid String topicParamInQuery) { final Object responseObject = networkCmProxyDataService.getResourceDataOperationalForCmHandle(cmHandle, resourceIdentifier, acceptParamInHeader, - optionsParamInQuery); + optionsParamInQuery, + topicParamInQuery); return ResponseEntity.ok(responseObject); } @@ -97,17 +100,20 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { * @param resourceIdentifier resource identifier * @param acceptParamInHeader accept header parameter * @param optionsParamInQuery options query parameter + * @param topicParamInQuery topic query parameter * @return {@code ResponseEntity} response from dmi plugin */ @Override public ResponseEntity getResourceDataRunningForCmHandle(final String cmHandle, final @NotNull @Valid String resourceIdentifier, final String acceptParamInHeader, - final @Valid String optionsParamInQuery) { + final @Valid String optionsParamInQuery, + final @Valid String topicParamInQuery) { final Object responseObject = networkCmProxyDataService.getResourceDataPassThroughRunningForCmHandle(cmHandle, resourceIdentifier, acceptParamInHeader, - optionsParamInQuery); + optionsParamInQuery, + topicParamInQuery); return ResponseEntity.ok(responseObject); } diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java index 5aaf1c31f..2495f3642 100755 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java @@ -30,6 +30,7 @@ import org.onap.cps.ncmp.rest.controller.NetworkCmProxyController; import org.onap.cps.ncmp.rest.controller.NetworkCmProxyInventoryController; import org.onap.cps.ncmp.rest.model.ErrorMessage; import org.onap.cps.spi.exceptions.CpsException; +import org.onap.cps.spi.exceptions.DataNodeNotFoundException; import org.onap.cps.spi.exceptions.DataValidationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -69,6 +70,11 @@ public class NetworkCmProxyRestExceptionHandler { return buildErrorResponse(HttpStatus.BAD_REQUEST, exception); } + @ExceptionHandler({DataNodeNotFoundException.class}) + public static ResponseEntity handleNotFoundExceptions(final CpsException exception) { + return buildErrorResponse(HttpStatus.NOT_FOUND, exception); + } + private static ResponseEntity buildErrorResponse(final HttpStatus status, final Exception exception) { if (exception.getCause() != null || !(exception instanceof CpsException)) { log.error("Exception occurred", exception); diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy index c99771443..9cbf626ba 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy @@ -24,6 +24,7 @@ package org.onap.cps.ncmp.rest.controller import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle +import spock.lang.Shared import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.PATCH import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete @@ -70,7 +71,10 @@ class NetworkCmProxyControllerSpec extends Specification { def requestBody = '{"some-key":"some-value"}' - def 'Get Resource Data from pass-through operational.' () { + @Shared + def NO_TOPIC = null + + def 'Get Resource Data from pass-through operational.'() { given: 'resource data url' def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational" + "?resourceIdentifier=parent/child&options=(a=1,b=2)" @@ -84,12 +88,40 @@ class NetworkCmProxyControllerSpec extends Specification { 1 * mockNetworkCmProxyDataService.getResourceDataOperationalForCmHandle('testCmHandle', 'parent/child', 'application/json', - '(a=1,b=2)') + '(a=1,b=2)', + NO_TOPIC) + and: 'response status is Ok' + response.status == HttpStatus.OK.value() + } + + def 'Get Resource Data from pass-through operational with #scenario.'() { + given: 'resource data url' + def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational" + + "?resourceIdentifier=parent/child&options=(a=1,b=2)${topicQueryParam}" + when: 'get data resource request is performed' + def response = mvc.perform( + get(getUrl) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON_VALUE) + ).andReturn().response + then: 'the NCMP data service is called with operational data for cm handle' + 1 * mockNetworkCmProxyDataService.getResourceDataOperationalForCmHandle('testCmHandle', + 'parent/child', + 'application/json', + '(a=1,b=2)', + expectedTopicName) and: 'response status is Ok' response.status == HttpStatus.OK.value() + where: 'the following parameters are used' + scenario | topicQueryParam || expectedTopicName + 'Url with valid topic' | "&topic=my-topic-name" || "my-topic-name" + 'No topic in url' | '' || NO_TOPIC + 'Null topic in url' | "&topic=null" || "null" + 'Empty topic in url' | "&topic=\"\"" || "\"\"" + 'Missing topic in url' | "&topic=" || "" } - def 'Get Resource Data from pass-through running with #scenario value in resource identifier param.' () { + def 'Get Resource Data from pass-through running with #scenario value in resource identifier param.'() { given: 'resource data url' def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" + "?resourceIdentifier=" + resourceIdentifier + "&options=(a=1,b=2)" @@ -97,7 +129,8 @@ class NetworkCmProxyControllerSpec extends Specification { mockNetworkCmProxyDataService.getResourceDataPassThroughRunningForCmHandle('testCmHandle', resourceIdentifier, 'application/json', - '(a=1,b=2)') >> '{valid-json}' + '(a=1,b=2)', + NO_TOPIC) >> '{valid-json}' when: 'get data resource request is performed' def response = mvc.perform( get(getUrl) diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy index 8004328bc..260ffd213 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy @@ -45,6 +45,7 @@ import static org.onap.cps.ncmp.rest.exceptions.NetworkCmProxyRestExceptionHandl import static org.onap.cps.ncmp.rest.exceptions.NetworkCmProxyRestExceptionHandlerSpec.ApiType.NCMPINVENTORY import static org.springframework.http.HttpStatus.BAD_REQUEST import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR +import static org.springframework.http.HttpStatus.NOT_FOUND import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post @@ -76,40 +77,39 @@ class NetworkCmProxyRestExceptionHandlerSpec extends Specification { def dataNodeBaseEndpointNcmpInventory @Shared - def errorMessage = 'some error message' + def sampleErrorMessage = 'some error message' @Shared - def errorDetails = 'some error details' - @Shared - def dataNodeNotFoundErrorMessage = 'DataNode not found' + def sampleErrorDetails = 'some error details' def setup() { dataNodeBaseEndpointNcmp = "$basePathNcmp/v1" dataNodeBaseEndpointNcmpInventory = "$basePathNcmpInventory/v1" } - def 'Get request with generic #scenario exception returns correct HTTP Status.'() { + def 'Get request with generic #scenario exception returns correct HTTP Status with #scenario'() { when: 'an exception is thrown by the service' setupTestException(exception, NCMP) def response = performTestRequest(NCMP) then: 'an HTTP response is returned with correct message and details' assertTestResponse(response, expectedErrorCode, expectedErrorMessage, expectedErrorDetails) where: - scenario | exception || expectedErrorDetails | expectedErrorMessage | expectedErrorCode - 'CPS' | new CpsException(errorMessage, errorDetails) || errorDetails | errorMessage | INTERNAL_SERVER_ERROR - 'NCMP-server' | new ServerNcmpException(errorMessage, errorDetails) || null | errorMessage | INTERNAL_SERVER_ERROR - 'NCMP-client' | new DmiRequestException(errorMessage, errorDetails) || null | errorMessage | BAD_REQUEST - 'DataNode Validation' | new DataNodeNotFoundException(dataNodeNotFoundErrorMessage, errorDetails) || null | dataNodeNotFoundErrorMessage | BAD_REQUEST - 'other' | new IllegalStateException(errorMessage) || null | errorMessage | INTERNAL_SERVER_ERROR + scenario | exception || expectedErrorDetails | expectedErrorMessage | expectedErrorCode + 'CPS' | new CpsException(sampleErrorMessage, sampleErrorDetails) || sampleErrorDetails | sampleErrorMessage | INTERNAL_SERVER_ERROR + 'NCMP-server' | new ServerNcmpException(sampleErrorMessage, sampleErrorDetails) || null | sampleErrorMessage | INTERNAL_SERVER_ERROR + 'NCMP-client' | new DmiRequestException(sampleErrorMessage, sampleErrorDetails) || null | sampleErrorMessage | BAD_REQUEST + 'DataNode Validation' | new DataNodeNotFoundException('myDataspaceName', 'myAnchorName') || null | 'DataNode not found' | NOT_FOUND + 'other' | new IllegalStateException(sampleErrorMessage) || null | sampleErrorMessage | INTERNAL_SERVER_ERROR + 'Data Node Not Found' | new DataNodeNotFoundException('myDataspaceName', 'myAnchorName') || 'DataNode not found' | 'DataNode not found' | NOT_FOUND } def 'Post request with exception returns correct HTTP Status.'() { given: 'the service throws data validation exception' - def exception = new DataValidationException(errorMessage, errorDetails) + def exception = new DataValidationException(sampleErrorMessage, sampleErrorDetails) setupTestException(exception, NCMPINVENTORY) when: 'the HTTP request is made' def response = performTestRequest(NCMPINVENTORY) then: 'an HTTP response is returned with correct message and details' - assertTestResponse(response, BAD_REQUEST, errorMessage, errorDetails) + assertTestResponse(response, BAD_REQUEST, sampleErrorMessage, sampleErrorDetails) } def setupTestException(exception, apiType) { @@ -130,9 +130,9 @@ class NetworkCmProxyRestExceptionHandlerSpec extends Specification { static void assertTestResponse(response, expectedStatus , expectedErrorMessage , expectedErrorDetails) { assert response.status == expectedStatus.value() def content = new JsonSlurper().parseText(response.contentAsString) - assert content['status'] == expectedStatus.toString() - assert content['message'] == expectedErrorMessage - assert expectedErrorDetails == null || content['details'] == expectedErrorDetails + assert content['status'].toString().contains(expectedStatus.toString()) + assert content['message'].toString().contains(expectedErrorMessage) + assert expectedErrorDetails == null || content['details'].toString().contains(expectedErrorDetails) } enum ApiType { diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java index 471e97e08..d942d26c8 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java @@ -49,12 +49,14 @@ public interface NetworkCmProxyDataService { * @param resourceIdentifier resource identifier * @param acceptParamInHeader accept param * @param optionsParamInQuery options query + * @param topicParamInQuery topic name for (triggering) async responses * @return {@code Object} resource data */ Object getResourceDataOperationalForCmHandle(String cmHandleId, String resourceIdentifier, String acceptParamInHeader, - String optionsParamInQuery); + String optionsParamInQuery, + String topicParamInQuery); /** * Get resource data for data store pass-through running @@ -64,12 +66,14 @@ public interface NetworkCmProxyDataService { * @param resourceIdentifier resource identifier * @param acceptParamInHeader accept param * @param optionsParamInQuery options query + * @param topicParamInQuery topic query * @return {@code Object} resource data */ Object getResourceDataPassThroughRunningForCmHandle(String cmHandleId, String resourceIdentifier, String acceptParamInHeader, - String optionsParamInQuery); + String optionsParamInQuery, + String topicParamInQuery); /** * Write resource data for data store pass-through running diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java index 1762e4628..e923ce414 100755 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java @@ -37,9 +37,12 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.regex.Pattern; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.logging.log4j.util.Strings; import org.onap.cps.api.CpsAdminService; import org.onap.cps.api.CpsDataService; import org.onap.cps.api.CpsModuleService; @@ -57,6 +60,7 @@ import org.onap.cps.spi.exceptions.DataNodeNotFoundException; import org.onap.cps.spi.exceptions.DataValidationException; import org.onap.cps.spi.model.ModuleReference; import org.onap.cps.utils.JsonObjectMapper; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @@ -81,6 +85,12 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService private final YangModelCmHandleRetriever yangModelCmHandleRetriever; + // valid kafka topic name regex + private static final Pattern TOPIC_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9]([._-](?![._-])|" + + "[a-zA-Z0-9]){0,120}[a-zA-Z0-9]$"); + private static final String NO_REQUEST_ID = null; + private static final String NO_TOPIC = null; + @Override public void updateDmiRegistrationAndSyncModule(final DmiPluginRegistration dmiPluginRegistration) { dmiPluginRegistration.validateDmiPluginRegistration(); @@ -104,26 +114,21 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService public Object getResourceDataOperationalForCmHandle(final String cmHandleId, final String resourceIdentifier, final String acceptParamInHeader, - final String optionsParamInQuery) { - return handleResponse(dmiDataOperations.getResourceDataFromDmi( - cmHandleId, - resourceIdentifier, - optionsParamInQuery, - acceptParamInHeader, - DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL), "Not able to get resource data."); + final String optionsParamInQuery, + final String topicParamInQuery) { + + return validateTopicNameAndGetResourceData(cmHandleId, resourceIdentifier, acceptParamInHeader, + DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL, optionsParamInQuery, topicParamInQuery); } @Override public Object getResourceDataPassThroughRunningForCmHandle(final String cmHandleId, final String resourceIdentifier, final String acceptParamInHeader, - final String optionsParamInQuery) { - return handleResponse(dmiDataOperations.getResourceDataFromDmi( - cmHandleId, - resourceIdentifier, - optionsParamInQuery, - acceptParamInHeader, - DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING), "Not able to get resource data."); + final String optionsParamInQuery, + final String topicParamInQuery) { + return validateTopicNameAndGetResourceData(cmHandleId, resourceIdentifier, acceptParamInHeader, + DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING, optionsParamInQuery, topicParamInQuery); } @Override @@ -297,4 +302,32 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService cpsAdminService.createAnchor(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, yangModelCmHandle.getId(), yangModelCmHandle.getId()); } -} + + private static boolean isValidTopicName(final String topicName) { + return Strings.isNotEmpty(topicName) && TOPIC_NAME_PATTERN.matcher(topicName).matches(); + } + + private Map buildDmiResponse(final String requestId) { + final Map dmiResponseMap = new HashMap<>(); + dmiResponseMap.put("requestId", requestId); + return dmiResponseMap; + } + + private Object validateTopicNameAndGetResourceData(final String cmHandleId, + final String resourceIdentifier, + final String acceptParamInHeader, + final DmiOperations.DataStoreEnum dataStore, + final String optionsParamInQuery, + final String topicParamInQuery) { + final boolean processAsynchronously = isValidTopicName(topicParamInQuery); + if (processAsynchronously) { + final String resourceDataRequestId = UUID.randomUUID().toString(); + return ResponseEntity.status(HttpStatus.OK) + .body(buildDmiResponse(resourceDataRequestId)); + } + final ResponseEntity responseEntity = dmiDataOperations.getResourceDataFromDmi( + cmHandleId, resourceIdentifier, optionsParamInQuery, acceptParamInHeader, + dataStore, NO_REQUEST_ID, NO_TOPIC); + return handleResponse(responseEntity, "Not able to get resource data."); + } +} \ No newline at end of file diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java index 229d4fc91..68de9d5c6 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java @@ -23,10 +23,10 @@ package org.onap.cps.ncmp.api.impl.operations; import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING; import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum; import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.READ; -import static org.onap.cps.ncmp.api.impl.operations.RequiredDmiService.DATA; import org.onap.cps.ncmp.api.impl.client.DmiRestClient; import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration; +import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder; import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.http.HttpHeaders; @@ -47,8 +47,8 @@ public class DmiDataOperations extends DmiOperations { public DmiDataOperations(final YangModelCmHandleRetriever cmHandlePropertiesRetriever, final JsonObjectMapper jsonObjectMapper, final NcmpConfiguration.DmiProperties dmiProperties, - final DmiRestClient dmiRestClient) { - super(cmHandlePropertiesRetriever, jsonObjectMapper, dmiProperties, dmiRestClient); + final DmiRestClient dmiRestClient, final DmiServiceUrlBuilder dmiServiceUrlBuilder) { + super(cmHandlePropertiesRetriever, jsonObjectMapper, dmiProperties, dmiRestClient, dmiServiceUrlBuilder); } /** @@ -59,25 +59,31 @@ public class DmiDataOperations extends DmiOperations { * @param resourceId resource identifier * @param optionsParamInQuery options query * @param acceptParamInHeader accept parameter - * @param dataStore data store enum + * @param dataStore data store enum + * @param requestId requestId for async responses + * @param topicParamInQuery topic name for (triggering) async responses * @return {@code ResponseEntity} response entity */ public ResponseEntity getResourceDataFromDmi(final String cmHandleId, - final String resourceId, - final String optionsParamInQuery, - final String acceptParamInHeader, - final DataStoreEnum dataStore) { + final String resourceId, + final String optionsParamInQuery, + final String acceptParamInHeader, + final DataStoreEnum dataStore, + final String requestId, + final String topicParamInQuery) { final YangModelCmHandle yangModelCmHandle = - yangModelCmHandleRetriever.getDmiServiceNamesAndProperties(cmHandleId); + yangModelCmHandleRetriever.getDmiServiceNamesAndProperties(cmHandleId); final DmiRequestBody dmiRequestBody = DmiRequestBody.builder() .operation(READ) + .requestId(requestId) .build(); dmiRequestBody.asDmiProperties(yangModelCmHandle.getDmiProperties()); final String jsonBody = jsonObjectMapper.asJsonString(dmiRequestBody); - final var dmiResourceDataUrl = getDmiDatastoreUrlWithOptions( - yangModelCmHandle.resolveDmiServiceName(DATA), cmHandleId, resourceId, - optionsParamInQuery, dataStore); + final var dmiResourceDataUrl = dmiServiceUrlBuilder.getDmiDatastoreUrl( + dmiServiceUrlBuilder.populateQueryParams(resourceId, optionsParamInQuery, + topicParamInQuery), dmiServiceUrlBuilder.populateUriVariables( + yangModelCmHandle, cmHandleId, dataStore)); final var httpHeaders = prepareHeader(acceptParamInHeader); return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonBody, httpHeaders); } @@ -108,33 +114,10 @@ public class DmiDataOperations extends DmiOperations { dmiRequestBody.asDmiProperties(yangModelCmHandle.getDmiProperties()); final String jsonBody = jsonObjectMapper.asJsonString(dmiRequestBody); final String dmiUrl = - getResourceInDataStoreUrl(yangModelCmHandle.resolveDmiServiceName(DATA), - cmHandleId, resourceId, PASSTHROUGH_RUNNING); + dmiServiceUrlBuilder.getDmiDatastoreUrl(dmiServiceUrlBuilder.populateQueryParams(resourceId, + null, null), + dmiServiceUrlBuilder.populateUriVariables(yangModelCmHandle, cmHandleId, PASSTHROUGH_RUNNING)); return dmiRestClient.postOperationWithJsonData(dmiUrl, jsonBody, new HttpHeaders()); } - private String getResourceInDataStoreUrl(final String dmiServiceName, - final String cmHandleId, - final String resourceId, - final DataStoreEnum dataStoreEnum) { - return getCmHandleUrl(dmiServiceName, cmHandleId) - + "data" - + URL_SEPARATOR - + "ds" - + URL_SEPARATOR - + dataStoreEnum.getValue() - + "?resourceIdentifier=" - + resourceId; - } - - private String getDmiDatastoreUrlWithOptions(final String dmiServiceName, - final String cmHandleId, - final String resourceId, - final String optionsParamInQuery, - final DataStoreEnum dataStoreEnum) { - final String resourceInDataStoreUrl = getResourceInDataStoreUrl(dmiServiceName, - cmHandleId, resourceId, dataStoreEnum); - return appendOptionsQuery(resourceInDataStoreUrl, optionsParamInQuery); - } - } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java index bfe934dfd..d79988e2e 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import org.onap.cps.ncmp.api.impl.client.DmiRestClient; import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration; +import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder; import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; import org.onap.cps.ncmp.api.models.YangResource; import org.onap.cps.spi.model.ModuleReference; @@ -53,8 +54,8 @@ public class DmiModelOperations extends DmiOperations { public DmiModelOperations(final YangModelCmHandleRetriever dmiPropertiesRetriever, final JsonObjectMapper jsonObjectMapper, final NcmpConfiguration.DmiProperties dmiProperties, - final DmiRestClient dmiRestClient) { - super(dmiPropertiesRetriever, jsonObjectMapper, dmiProperties, dmiRestClient); + final DmiRestClient dmiRestClient, final DmiServiceUrlBuilder dmiServiceUrlBuilder) { + super(dmiPropertiesRetriever, jsonObjectMapper, dmiProperties, dmiRestClient, dmiServiceUrlBuilder); } /** diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java index 645d9799f..75ba91b4f 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java @@ -20,17 +20,15 @@ package org.onap.cps.ncmp.api.impl.operations; -import com.google.common.base.Strings; import lombok.Getter; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.onap.cps.ncmp.api.impl.client.DmiRestClient; import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration; +import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; -@Slf4j @RequiredArgsConstructor @Service public class DmiOperations { @@ -39,7 +37,7 @@ public class DmiOperations { public enum DataStoreEnum { PASSTHROUGH_OPERATIONAL("ncmp-datastore:passthrough-operational"), PASSTHROUGH_RUNNING("ncmp-datastore:passthrough-running"); - private String value; + private final String value; DataStoreEnum(final String value) { this.value = value; @@ -50,30 +48,12 @@ public class DmiOperations { protected final JsonObjectMapper jsonObjectMapper; protected final NcmpConfiguration.DmiProperties dmiProperties; protected final DmiRestClient dmiRestClient; - - static final String URL_SEPARATOR = "/"; - - String getCmHandleUrl(final String dmiServiceName, final String cmHandle) { - return dmiServiceName - + dmiProperties.getDmiBasePath() - + URL_SEPARATOR - + "v1" - + URL_SEPARATOR - + "ch" - + URL_SEPARATOR - + cmHandle - + URL_SEPARATOR; - } + protected final DmiServiceUrlBuilder dmiServiceUrlBuilder; String getDmiResourceUrl(final String dmiServiceName, final String cmHandle, final String resourceName) { - return getCmHandleUrl(dmiServiceName, cmHandle) + resourceName; - } - - static String appendOptionsQuery(final String url, final String optionsParamInQuery) { - if (Strings.isNullOrEmpty(optionsParamInQuery)) { - return url; - } - return url + "&options=" + optionsParamInQuery; + return dmiServiceUrlBuilder.getCmHandleUrl() + .pathSegment("{resourceName}") + .buildAndExpand(dmiServiceName, dmiProperties.getDmiBasePath(), cmHandle, resourceName).toUriString(); } static HttpHeaders prepareHeader(final String acceptParam) { diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiRequestBody.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiRequestBody.java index d97e90cbb..c84e4cb87 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiRequestBody.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiRequestBody.java @@ -58,6 +58,7 @@ public class DmiRequestBody { private String data; @JsonProperty("cmHandleProperties") private Map dmiProperties; + private String requestId; /** * Set DMI Properties by converting a list of YangModelCmHandle.Property objects. diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java new file mode 100644 index 000000000..b60aac951 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java @@ -0,0 +1,124 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation + * ================================================================================ + * 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.ncmp.api.impl.utils; + +import static org.onap.cps.ncmp.api.impl.operations.RequiredDmiService.DATA; + +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.apache.logging.log4j.util.Strings; +import org.apache.logging.log4j.util.TriConsumer; +import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration; +import org.onap.cps.ncmp.api.impl.operations.DmiOperations; +import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.util.UriComponentsBuilder; + +@Component +@RequiredArgsConstructor +public class DmiServiceUrlBuilder { + + private final NcmpConfiguration.DmiProperties dmiProperties; + + /** + * This method creates the dmi service url. + * + * @param queryParams query param map as key,value pair + * @param uriVariables uri param map as key (placeholder),value pair + * @return {@code String} dmi service url as string + */ + public String getDmiDatastoreUrl(final MultiValueMap queryParams, + final Map uriVariables) { + final UriComponentsBuilder uriComponentsBuilder = getCmHandleUrl() + .pathSegment("data") + .pathSegment("ds") + .pathSegment("{dataStore}") + .queryParams(queryParams) + .uriVariables(uriVariables); + return uriComponentsBuilder.buildAndExpand().toUriString(); + } + + /** + * This method creates the dmi service url builder object with path variables. + * + * @return {@code UriComponentsBuilder} dmi service url builder object + */ + public UriComponentsBuilder getCmHandleUrl() { + return UriComponentsBuilder.newInstance() + .path("{dmiServiceName}") + .pathSegment("{dmiBasePath}") + .pathSegment("v1") + .pathSegment("ch") + .pathSegment("{cmHandle}"); + } + + /** + * This method populates uri variables. + * + * @param yangModelCmHandle get dmi service name + * @param cmHandle cm handle name for dmi registration + * @return {@code String} dmi service url as string + */ + public Map populateUriVariables(final YangModelCmHandle yangModelCmHandle, + final String cmHandle, + final DmiOperations.DataStoreEnum dataStore) { + final Map uriVariables = new HashMap<>(); + final String dmiBasePath = dmiProperties.getDmiBasePath(); + uriVariables.put("dmiServiceName", + yangModelCmHandle.resolveDmiServiceName(DATA)); + uriVariables.put("dmiBasePath", dmiBasePath); + uriVariables.put("cmHandle", cmHandle); + uriVariables.put("dataStore", dataStore.getValue()); + return uriVariables; + } + + /** + * This method is used to populate map from query params. + * + * @param resourceId unique id of response for valid topic + * @param optionsParamInQuery options into url param + * @param topicParamInQuery topic into url param + * @return all valid query params as map + */ + public MultiValueMap populateQueryParams(final String resourceId, + final String optionsParamInQuery, + final String topicParamInQuery) { + final MultiValueMap queryParams = new LinkedMultiValueMap<>(); + getQueryParamConsumer().accept("resourceIdentifier", + resourceId, queryParams); + getQueryParamConsumer().accept("options", optionsParamInQuery, queryParams); + if (Strings.isNotEmpty(topicParamInQuery)) { + getQueryParamConsumer().accept("topic", topicParamInQuery, queryParams); + } + return queryParams; + } + + private TriConsumer> getQueryParamConsumer() { + return (paramName, paramValue, paramMap) -> { + if (Strings.isNotEmpty(paramValue)) { + paramMap.add(paramName, paramValue); + } + }; + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy index b2a3d77ca..e6d18d915 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy @@ -24,6 +24,7 @@ package org.onap.cps.ncmp.api.impl import org.onap.cps.ncmp.api.impl.operations.YangModelCmHandleRetriever import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle +import spock.lang.Shared import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING @@ -56,6 +57,10 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { def mockDmiDataOperations = Mock(DmiDataOperations) def nullNetworkCmProxyDataServicePropertyHandler = null def mockYangModelCmHandleRetriever = Mock(YangModelCmHandleRetriever) + def NO_TOPIC = null + def NO_REQUEST_ID = null + @Shared + def OPTIONS_PARAM = '(a=1,b=2)' def objectUnderTest = new NetworkCmProxyDataServiceImpl(mockCpsDataService, spiedJsonObjectMapper, mockDmiDataOperations, mockDmiModelOperations, mockCpsModuleService, mockCpsAdminService, nullNetworkCmProxyDataServicePropertyHandler, mockYangModelCmHandleRetriever) @@ -64,7 +69,6 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { def dataNode = new DataNode(leaves: ['dmi-service-name': 'testDmiService']) - def 'Write resource data for pass-through running from DMI using POST #scenario cm handle properties.'() { given: 'cpsDataService returns valid datanode' mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', @@ -104,18 +108,21 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode and: 'get resource data from DMI is called' mockDmiDataOperations.getResourceDataFromDmi( - 'testCmHandle', - 'testResourceId', - '(a=1,b=2)', - 'testAcceptParam' , - PASSTHROUGH_OPERATIONAL) >> new ResponseEntity<>('result-json', HttpStatus.OK) + 'testCmHandle', + 'testResourceId', + OPTIONS_PARAM, + 'testAcceptParam', + PASSTHROUGH_OPERATIONAL, + NO_REQUEST_ID, + NO_TOPIC) >> new ResponseEntity<>('dmi-response', HttpStatus.OK) when: 'get resource data operational for cm-handle is called' def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle', - 'testResourceId', - 'testAcceptParam', - '(a=1,b=2)') + 'testResourceId', + 'testAcceptParam', + OPTIONS_PARAM, + NO_TOPIC) then: 'DMI returns a json response' - response == 'result-json' + response == 'dmi-response' } def 'Get resource data for pass-through operational from DMI with Json Processing Exception.'() { @@ -129,9 +136,10 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { >> new ResponseEntity<>('NOK-json', HttpStatus.NOT_FOUND) when: 'get resource data is called' objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle', - 'testResourceId', - 'testAcceptParam', - '(a=1,b=2)') + 'testResourceId', + 'testAcceptParam', + OPTIONS_PARAM, + NO_TOPIC) then: 'exception is thrown with the expected details' def exceptionThrown = thrown(ServerNcmpException.class) exceptionThrown.details == 'DMI status code: 404, DMI response body: NOK-json' @@ -143,16 +151,19 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode and: 'DMI returns NOK response' mockDmiDataOperations.getResourceDataFromDmi('testCmHandle', - 'testResourceId', - '(a=1,b=2)', - 'testAcceptParam', - PASSTHROUGH_OPERATIONAL) - >> new ResponseEntity<>('NOK-json', HttpStatus.NOT_FOUND) + 'testResourceId', + OPTIONS_PARAM, + 'testAcceptParam', + PASSTHROUGH_OPERATIONAL, + NO_REQUEST_ID, + NO_TOPIC) + >> new ResponseEntity<>('NOK-json', HttpStatus.NOT_FOUND) when: 'get resource data is called' objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle', - 'testResourceId', - 'testAcceptParam', - '(a=1,b=2)') + 'testResourceId', + 'testAcceptParam', + OPTIONS_PARAM, + NO_TOPIC) then: 'exception is thrown' def exceptionThrown = thrown(ServerNcmpException.class) and: 'details contains the original response' @@ -165,17 +176,20 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode and: 'DMI returns valid response and data' mockDmiDataOperations.getResourceDataFromDmi('testCmHandle', - 'testResourceId', - '(a=1,b=2)', - 'testAcceptParam', - PASSTHROUGH_RUNNING) >> new ResponseEntity<>('{result-json}', HttpStatus.OK) + 'testResourceId', + OPTIONS_PARAM, + 'testAcceptParam', + PASSTHROUGH_RUNNING, + NO_REQUEST_ID, + NO_TOPIC) >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK) when: 'get resource data is called' def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle', - 'testResourceId', - 'testAcceptParam', - '(a=1,b=2)') + 'testResourceId', + 'testAcceptParam', + OPTIONS_PARAM, + NO_TOPIC) then: 'get resource data returns expected response' - response == '{result-json}' + response == '{dmi-response}' } def 'Get resource data for pass-through running from DMI return NOK response.'() { @@ -184,22 +198,91 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode and: 'DMI returns NOK response' mockDmiDataOperations.getResourceDataFromDmi('testCmHandle', - 'testResourceId', - '(a=1,b=2)', - 'testAcceptParam', - PASSTHROUGH_RUNNING) - >> new ResponseEntity<>('NOK-json', HttpStatus.NOT_FOUND) + 'testResourceId', + OPTIONS_PARAM, + 'testAcceptParam', + PASSTHROUGH_RUNNING, + NO_REQUEST_ID, + NO_TOPIC) + >> new ResponseEntity<>('NOK-json', HttpStatus.NOT_FOUND) when: 'get resource data is called' objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle', - 'testResourceId', - 'testAcceptParam', - '(a=1,b=2)') + 'testResourceId', + 'testAcceptParam', + OPTIONS_PARAM, + NO_TOPIC) then: 'exception is thrown' def exceptionThrown = thrown(ServerNcmpException.class) and: 'details contains the original response' exceptionThrown.details.contains('NOK-json') } + def 'Get resource data for operational from DMI with empty topic sync request.'() { + given: 'cps data service returns valid data node' + mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', + cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode + and: 'dmi data operation returns valid response and data' + mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, NO_REQUEST_ID, NO_TOPIC) + >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK) + when: 'get resource data is called data operational with blank topic' + def responseData = objectUnderTest.getResourceDataOperationalForCmHandle('', '', + '', '', emptyTopic) + then: '(synchronous) the dmi response is expected' + assert responseData == '{dmi-response}' + where: 'the following parameters are used' + scenario | emptyTopic + 'No topic in url' | '' + 'Null topic in url' | null + 'Empty topic in url' | '\"\"' + 'Blank topic in url' | ' ' + } + + def 'Get resource data for data operational from DMI with valid topic i.e. async request.'() { + given: 'cps data service returns valid data node' + mockCpsDataService.getDataNode(*_) >> dataNode + and: 'dmi data operation returns valid response and data' + mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, _, 'my-topic-name') + >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK) + when: 'get resource data is called for data operational with valid topic' + def responseData = objectUnderTest.getResourceDataOperationalForCmHandle('', '', '', '', 'my-topic-name') + then: 'non empty request id is generated' + assert responseData.body.requestId.length() > 0 + } + + def 'Get resource data for pass through running from DMI with valid topic async request.'() { + given: 'cps data service returns valid data node' + mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', + cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode + and: 'dmi data operation returns valid response and data' + mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, _, 'my-topic-name') + >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK) + when: 'get resource data is called for data operational with valid topic' + def responseData = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('', + '', '', OPTIONS_PARAM, 'my-topic-name') + then: 'non empty request id is generated' + assert responseData.body.requestId.length() > 0 + } + + def 'Get resource data for pass through running from DMI sync request where #scenario.'() { + given: 'cps data service returns valid data node' + mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', + cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode + and: 'dmi data operation returns valid response and data' + mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, NO_REQUEST_ID, NO_TOPIC) + >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK) + when: 'get resource data is called for data operational with valid topic' + def responseData = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('', + '', '', '', emptyTopic) + then: '(synchronous) the dmi response is expected' + assert responseData == '{dmi-response}' + where: 'the following parameters are used' + scenario | emptyTopic + 'No topic in url' | '' + 'Null topic in url' | null + 'Empty topic in url' | '\"\"' + 'Blank topic in url' | ' ' + } + def 'Getting Yang Resources.'() { when: 'yang resources is called' objectUnderTest.getYangResourcesModuleReferences('some cm handle') @@ -255,7 +338,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { givenOperation, '{some-json}', 'application/json') - then: 'an exception is thrown with the expected error message detailsd with correct operation' + then: 'an exception is thrown with the expected error message details with correct operation' def exceptionThrown = thrown(ServerNcmpException.class) exceptionThrown.getMessage().contains(expectedResponseMessage) where: diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy index e585825ca..3df862ac5 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy @@ -22,12 +22,15 @@ package org.onap.cps.ncmp.api.impl.operations import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration +import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder import org.onap.cps.utils.JsonObjectMapper import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.http.ResponseEntity import org.springframework.test.context.ContextConfiguration +import org.springframework.util.MultiValueMap +import spock.lang.Shared import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING @@ -40,43 +43,54 @@ import org.springframework.http.HttpStatus class DmiDataOperationsSpec extends DmiOperationsBaseSpec { @SpringBean - JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) + DmiServiceUrlBuilder dmiServiceUrlBuilder = Mock() + def dmiServiceBaseUrl = "${dmiServiceName}/dmi/v1/ch/${cmHandleId}/data/ds/ncmp-datastore:" + def NO_TOPIC = null + def NO_REQUEST_ID = null + @Shared + def OPTIONS_PARAM = '(a=1,b=2)' + + @SpringBean + JsonObjectMapper spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper())) @Autowired DmiDataOperations objectUnderTest - def 'call get resource data for #expectedDatastoreInUrl from DMI #scenario.'() { + def 'call get resource data for #expectedDatastoreInUrl from DMI without topic #scenario.'() { given: 'a cm handle for #cmHandleId' mockYangModelCmHandleRetrieval(dmiProperties) and: 'a positive response from DMI service when it is called with the expected parameters' def responseFromDmi = new ResponseEntity(HttpStatus.OK) - mockDmiRestClient.postOperationWithJsonData( - "${dmiServiceName}/dmi/v1/ch/${cmHandleId}/data/ds/ncmp-datastore:${expectedDatastoreInUrl}?resourceIdentifier=${resourceIdentifier}${expectedOptionsInUrl}", - expectedJson, [Accept:['sample accept header']]) >> responseFromDmi + def expectedUrl = dmiServiceBaseUrl + "${expectedDatastoreInUrl}?resourceIdentifier=${resourceIdentifier}${expectedOptionsInUrl}" + mockDmiRestClient.postOperationWithJsonData(expectedUrl, + expectedJson, [Accept: ['sample accept header']]) >> responseFromDmi + dmiServiceUrlBuilder.getDmiDatastoreUrl(_, _) >> expectedUrl when: 'get resource data is invoked' - def result = objectUnderTest.getResourceDataFromDmi(cmHandleId,resourceIdentifier, options,'sample accept header', dataStore) + def result = objectUnderTest.getResourceDataFromDmi(cmHandleId, resourceIdentifier, + options, 'sample accept header', dataStore, NO_REQUEST_ID, NO_TOPIC) then: 'the result is the response from the DMI service' assert result == responseFromDmi where: 'the following parameters are used' - scenario | dmiProperties | dataStore | options || expectedJson | expectedDatastoreInUrl | expectedOptionsInUrl - 'without properties' | [] | PASSTHROUGH_OPERATIONAL | '(a=1,b=2)' || '{"operation":"read","cmHandleProperties":{}}' | 'passthrough-operational' | '&options=(a=1,b=2)' - 'with properties' | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | '(a=1,b=2)' || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-operational' | '&options=(a=1,b=2)' - 'null options' | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | null || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-operational' | '' - 'empty options' | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | '' || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-operational' | '' - 'datastore running' | [] | PASSTHROUGH_RUNNING | '(a=1,b=2)' || '{"operation":"read","cmHandleProperties":{}}' | 'passthrough-running' | '&options=(a=1,b=2)' + scenario | dmiProperties | dataStore | options || expectedJson | expectedDatastoreInUrl | expectedOptionsInUrl + 'without properties' | [] | PASSTHROUGH_OPERATIONAL | OPTIONS_PARAM || '{"operation":"read","cmHandleProperties":{}}' | 'passthrough-operational' | '&options=(a=1,b=2)' + 'with properties' | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | OPTIONS_PARAM || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-operational' | '&options=(a=1,b=2)' + 'null options' | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | null || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-operational' | '' + 'empty options' | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | '' || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-operational' | '' + 'datastore running without properties' | [] | PASSTHROUGH_RUNNING | OPTIONS_PARAM || '{"operation":"read","cmHandleProperties":{}}' | 'passthrough-running' | '&options=(a=1,b=2)' + 'datastore running with properties' | [yangModelCmHandleProperty] | PASSTHROUGH_RUNNING | OPTIONS_PARAM || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-running' | '&options=(a=1,b=2)' } def 'Write data for pass-through:running datastore in DMI.'() { given: 'a cm handle for #cmHandleId' mockYangModelCmHandleRetrieval([yangModelCmHandleProperty]) and: 'a positive response from DMI service when it is called with the expected parameters' - def expectedUrl = "${dmiServiceName}/dmi/v1/ch/${cmHandleId}/data/ds" + - "/ncmp-datastore:passthrough-running?resourceIdentifier=${resourceIdentifier}" + def expectedUrl = dmiServiceBaseUrl + "passthrough-running?resourceIdentifier=${resourceIdentifier}" def expectedJson = '{"operation":"' + expectedOperationInUrl + '","dataType":"some data type","data":"requestData","cmHandleProperties":{"prop1":"val1"}}' def responseFromDmi = new ResponseEntity(HttpStatus.OK) + dmiServiceUrlBuilder.getDmiDatastoreUrl(_, _) >> expectedUrl mockDmiRestClient.postOperationWithJsonData(expectedUrl, expectedJson, [:]) >> responseFromDmi when: 'write resource method is invoked' - def result = objectUnderTest.writeResourceDataPassThroughRunningFromDmi(cmHandleId,'parent/child', operation, 'requestData', 'some data type') + def result = objectUnderTest.writeResourceDataPassThroughRunningFromDmi(cmHandleId, 'parent/child', operation, 'requestData', 'some data type') then: 'the result is the response from the DMI service' assert result == responseFromDmi where: 'the following operation is performed' @@ -84,5 +98,4 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { CREATE || 'create' UPDATE || 'update' } - } \ No newline at end of file diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy index cd2cb7112..d3fc17cc0 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy @@ -23,6 +23,7 @@ package org.onap.cps.ncmp.api.impl.operations import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration +import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder import org.onap.cps.spi.model.ModuleReference import org.onap.cps.utils.JsonObjectMapper import org.spockframework.spring.SpringBean @@ -31,6 +32,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.test.context.ContextConfiguration +import org.springframework.web.util.UriComponentsBuilder import spock.lang.Shared @SpringBootTest @@ -50,14 +52,15 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec { given: 'a cm handle' mockYangModelCmHandleRetrieval([]) and: 'a positive response from DMI service when it is called with the expected parameters' - def moduleReferencesAsLisOfMaps = [[moduleName:'mod1',revision:'A'],[moduleName:'mod2',revision:'X']] - def responseFromDmi = new ResponseEntity([schemas:moduleReferencesAsLisOfMaps], HttpStatus.OK) - mockDmiRestClient.postOperationWithJsonData("${dmiServiceName}/dmi/v1/ch/${cmHandleId}/modules", - '{"cmHandleProperties":{}}', [:]) >> responseFromDmi + def moduleReferencesAsLisOfMaps = [[moduleName: 'mod1', revision: 'A'], [moduleName: 'mod2', revision: 'X']] + def expectedUrl = "${dmiServiceName}/dmi/v1/ch/${cmHandleId}/modules" + def responseFromDmi = new ResponseEntity([schemas: moduleReferencesAsLisOfMaps], HttpStatus.OK) + mockDmiRestClient.postOperationWithJsonData(expectedUrl, '{"cmHandleProperties":{}}', [:]) + >> responseFromDmi when: 'get module references is called' def result = objectUnderTest.getModuleReferences(yangModelCmHandle) then: 'the result consists of expected module references' - assert result == [new ModuleReference(moduleName:'mod1',revision:'A'), new ModuleReference(moduleName:'mod2',revision:'X')] + assert result == [new ModuleReference(moduleName: 'mod1', revision: 'A'), new ModuleReference(moduleName: 'mod2', revision: 'X')] } def 'Retrieving module references edge case: #scenario.'() { diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy index dd0d64dd6..e6f63ce1a 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy @@ -22,7 +22,9 @@ package org.onap.cps.ncmp.api.impl.operations import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.ncmp.api.impl.client.DmiRestClient +import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle +import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder import org.spockframework.spring.SpringBean import spock.lang.Shared import spock.lang.Specification @@ -41,6 +43,9 @@ abstract class DmiOperationsBaseSpec extends Specification { @SpringBean ObjectMapper spyObjectMapper = Spy() + @SpringBean + DmiServiceUrlBuilder dmiServiceUrlBuilder = new DmiServiceUrlBuilder(new NcmpConfiguration.DmiProperties()) + def yangModelCmHandle = new YangModelCmHandle() def static dmiServiceName = 'some service name' def static cmHandleId = 'some cm handle' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/utils/DmiServiceUrlBuilderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/utils/DmiServiceUrlBuilderSpec.groovy new file mode 100644 index 000000000..1615d055d --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/utils/DmiServiceUrlBuilderSpec.groovy @@ -0,0 +1,79 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation + * ================================================================================ + * 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.ncmp.api.utils + +import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING + +import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle +import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration +import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder +import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle +import spock.lang.Shared +import spock.lang.Specification + +class DmiServiceUrlBuilderSpec extends Specification { + + @Shared + YangModelCmHandle yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle("dmiServiceName", + "dmiDataServiceName", "dmiModuleServiceName", new NcmpServiceCmHandle()) + + NcmpConfiguration.DmiProperties dmiProperties = new NcmpConfiguration.DmiProperties(); + + def objectUnderTest = new DmiServiceUrlBuilder(dmiProperties) + + def 'Create the dmi service url with #scenario.'() { + given: 'uri variables' + dmiProperties.dmiBasePath = 'dmi'; + def uriVars = objectUnderTest.populateUriVariables(yangModelCmHandle, + "cmHandle", PASSTHROUGH_RUNNING); + and: 'query params' + def uriQueries = objectUnderTest.populateQueryParams(resourceId, + 'optionsParamInQuery', topicParamInQuery); + when: 'a dmi datastore service url is generated' + def dmiServiceUrl = objectUnderTest.getDmiDatastoreUrl(uriQueries, uriVars) + then: 'service url is generated as expected' + assert dmiServiceUrl == expectedDmiServiceUrl + where: 'the following parameters are used' + scenario | topicParamInQuery | resourceId || expectedDmiServiceUrl + 'With valid resourceId' | 'topicParamInQuery' | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery&topic=topicParamInQuery' + 'With Empty resourceId' | 'topicParamInQuery' | '' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?options=optionsParamInQuery&topic=topicParamInQuery' + 'With Empty dmi base path' | 'topicParamInQuery' | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery&topic=topicParamInQuery' + 'With Empty topicParamInQuery' | '' | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery' + } + + def 'Populate dmi data store url #scenario.'() { + given: 'uri variables are created' + dmiProperties.dmiBasePath = dmiBasePath; + def uriVars = objectUnderTest.populateUriVariables(yangModelCmHandle, + "cmHandle", PASSTHROUGH_RUNNING); + and: 'null query params' + def uriQueries = objectUnderTest.populateQueryParams(null, + null, null); + when: 'a dmi datastore service url is generated' + def dmiServiceUrl = objectUnderTest.getDmiDatastoreUrl(uriQueries, uriVars) + then: 'the created dmi service url matches the expected' + assert dmiServiceUrl == expectedDmiServiceUrl + where: 'the following parameters are used' + scenario | decription | dmiBasePath || expectedDmiServiceUrl + 'with base path / ' | 'Invalid base path as it starts with /' | '/dmi' || 'dmiServiceName//dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running' + 'without base path / ' | 'Valid path as it does not starts with /' | 'dmi' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running' + } +} diff --git a/cps-ncmp-service/src/test/resources/application.yml b/cps-ncmp-service/src/test/resources/application.yml index d8fbb64c5..c23926e4e 100644 --- a/cps-ncmp-service/src/test/resources/application.yml +++ b/cps-ncmp-service/src/test/resources/application.yml @@ -1,5 +1,5 @@ # ============LICENSE_START======================================================= -# Copyright (C) 2021 Nordix Foundation +# Copyright (C) 2021-2022 Nordix Foundation # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,5 +21,5 @@ dmi: username: some-user password: some-password api: - base-path: /dmi + base-path: dmi