From d32e708e011f1748f195472267b48fe7e7357798 Mon Sep 17 00:00:00 2001 From: seanbeirne Date: Thu, 30 Oct 2025 14:33:15 +0000 Subject: [PATCH] Introduce PUT for ProvMnS interface - Restructure Error Handling for ProvMnS - Create error response builder - Refactor GET error reponse - Changed provmns return object from resource to generic object to support error handling Issue-ID: CPS-2702 Change-Id: Ib3dfac611b5e26b6ad6039ed83b460fdcae492ba Signed-off-by: seanbeirne --- .../NetworkCmProxyRestExceptionHandler.java | 7 +- .../org/onap/cps/ncmp/rest/controller/ProvMnS.java | 6 +- .../ncmp/rest/controller/ProvMnsController.java | 181 +++++++++++++----- .../ncmp/rest/provmns/ErrorResponseBuilder.java | 82 +++++++++ .../NetworkCmProxyRestExceptionHandlerSpec.groovy | 38 ++-- .../rest/controller/ProvMnsControllerSpec.groovy | 204 ++++++++++++++++----- .../rest/util/ProvMnSParametersMapperSpec.groovy | 63 ------- cps-ncmp-service/pom.xml | 2 +- .../cps/ncmp/api/exceptions/ProvMnSException.java | 24 ++- .../policyexecutor/DeleteOperationDetails.java | 4 +- .../impl/data/policyexecutor/OperationDetails.java | 30 +++ .../impl/data/policyexecutor/OperationEntry.java | 38 ++++ .../impl/data/policyexecutor/PolicyExecutor.java | 56 ++++++ .../org/onap/cps/ncmp/impl/dmi/DmiRestClient.java | 28 ++- .../cps/ncmp/impl/provmns/ParameterMapper.java | 53 ++---- .../cps/ncmp/impl/provmns/ParametersBuilder.java | 57 +++--- .../ncmp/impl/provmns/RequestPathParameters.java | 42 +++++ .../onap/cps/ncmp/impl/provmns/model/Resource.java | 1 - .../PolicyExecutorConfigurationSpec.groovy | 3 +- .../data/policyexecutor/PolicyExecutorSpec.groovy | 36 +++- ...andleQueryParametersParameterMapperSpec.groovy} | 2 +- .../ncmp/impl/provmns/ParametersBuilderSpec.groovy | 39 ++++ ...Spec.groovy => TopicParameterMapperSpec.groovy} | 2 +- .../onap/cps/integration/base/DmiDispatcher.groovy | 2 +- .../ncmp/provmns/ProvMnSRestApiSpec.groovy | 12 +- 25 files changed, 753 insertions(+), 259 deletions(-) create mode 100644 cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/ErrorResponseBuilder.java delete mode 100644 cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/util/ProvMnSParametersMapperSpec.groovy rename cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/exception/InvalidPathException.java => cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/exceptions/ProvMnSException.java (68%) rename cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/model/ConfigurationManagementDeleteInput.java => cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/DeleteOperationDetails.java (86%) create mode 100644 cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetails.java create mode 100644 cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationEntry.java rename cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/ProvMnsRequestParameters.java => cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterMapper.java (53%) rename cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/ProvMnSParametersMapper.java => cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParametersBuilder.java (56%) create mode 100644 cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/RequestPathParameters.java rename cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/{CmHandleQueryParametersValidatorSpec.groovy => CmHandleQueryParametersParameterMapperSpec.groovy} (98%) create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParametersBuilderSpec.groovy rename cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/utils/events/{TopicValidatorSpec.groovy => TopicParameterMapperSpec.groovy} (97%) diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandler.java index 4bb07b607e..0b04432dd5 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandler.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandler.java @@ -37,11 +37,11 @@ import org.onap.cps.ncmp.api.exceptions.InvalidTopicException; import org.onap.cps.ncmp.api.exceptions.NcmpException; import org.onap.cps.ncmp.api.exceptions.PayloadTooLargeException; import org.onap.cps.ncmp.api.exceptions.PolicyExecutorException; +import org.onap.cps.ncmp.api.exceptions.ProvMnSException; import org.onap.cps.ncmp.api.exceptions.ServerNcmpException; import org.onap.cps.ncmp.rest.model.DmiErrorMessage; import org.onap.cps.ncmp.rest.model.DmiErrorMessageDmiResponse; import org.onap.cps.ncmp.rest.model.ErrorMessage; -import org.onap.cps.ncmp.rest.provmns.exception.InvalidPathException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -52,7 +52,8 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; * Exception handler with error message return. */ @Slf4j -@RestControllerAdvice(assignableTypes = {NetworkCmProxyController.class, NetworkCmProxyInventoryController.class}) +@RestControllerAdvice(assignableTypes = {NetworkCmProxyController.class, + NetworkCmProxyInventoryController.class}) @NoArgsConstructor(access = AccessLevel.PACKAGE) public class NetworkCmProxyRestExceptionHandler { @@ -102,7 +103,7 @@ public class NetworkCmProxyRestExceptionHandler { return buildErrorResponse(HttpStatus.PAYLOAD_TOO_LARGE, exception); } - @ExceptionHandler({InvalidPathException.class}) + @ExceptionHandler({ProvMnSException.class}) public static ResponseEntity invalidPathExceptions(final Exception exception) { return buildErrorResponse(HttpStatus.UNPROCESSABLE_ENTITY, exception); } diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnS.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnS.java index 9139db82b3..68b6151fc3 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnS.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnS.java @@ -101,7 +101,7 @@ public interface ProvMnS { produces = { "application/json"} ) - ResponseEntity getMoi( + ResponseEntity getMoi( HttpServletRequest httpServletRequest, @Parameter(name = "scope", description = "This parameter extends the set of targeted resources beyond the " + "base resource identified with the path component of the URI. " @@ -206,7 +206,7 @@ public interface ProvMnS { consumes = { "application/json-patch+json", "application/3gpp-json-patch+json" } ) - ResponseEntity patchMoi( + ResponseEntity patchMoi( HttpServletRequest httpServletRequest, @Parameter(name = "Resource", description = "The request body describes changes to be made to the target " + "resources. The following patch media types are available " @@ -273,7 +273,7 @@ public interface ProvMnS { consumes = { "application/json" } ) - ResponseEntity putMoi( + ResponseEntity putMoi( HttpServletRequest httpServletRequest, @Parameter(name = "Resource", description = "The request body describes the resource that has been created or replaced", required = true) diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnsController.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnsController.java index f3fc8b206b..3286dc566a 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnsController.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnsController.java @@ -24,20 +24,23 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.List; import lombok.RequiredArgsConstructor; import org.onap.cps.ncmp.api.data.models.OperationType; +import org.onap.cps.ncmp.api.exceptions.ProvMnSException; +import org.onap.cps.ncmp.api.inventory.models.CmHandleState; +import org.onap.cps.ncmp.exceptions.NoAlternateIdMatchFoundException; import org.onap.cps.ncmp.impl.data.policyexecutor.PolicyExecutor; import org.onap.cps.ncmp.impl.dmi.DmiRestClient; import org.onap.cps.ncmp.impl.inventory.InventoryPersistence; import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle; import org.onap.cps.ncmp.impl.models.RequiredDmiService; +import org.onap.cps.ncmp.impl.provmns.ParameterMapper; +import org.onap.cps.ncmp.impl.provmns.ParametersBuilder; +import org.onap.cps.ncmp.impl.provmns.RequestPathParameters; import org.onap.cps.ncmp.impl.provmns.model.ClassNameIdGetDataNodeSelectorParameter; import org.onap.cps.ncmp.impl.provmns.model.Resource; import org.onap.cps.ncmp.impl.provmns.model.Scope; import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher; -import org.onap.cps.ncmp.impl.utils.http.RestServiceUrlTemplateBuilder; import org.onap.cps.ncmp.impl.utils.http.UrlTemplateParameters; -import org.onap.cps.ncmp.rest.provmns.model.ConfigurationManagementDeleteInput; -import org.onap.cps.ncmp.rest.util.ProvMnSParametersMapper; -import org.onap.cps.ncmp.rest.util.ProvMnsRequestParameters; +import org.onap.cps.ncmp.rest.provmns.ErrorResponseBuilder; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -50,75 +53,153 @@ import org.springframework.web.bind.annotation.RestController; public class ProvMnsController implements ProvMnS { private static final String NO_AUTHORIZATION = null; + private static final String PROVMNS_NOT_SUPPORTED_ERROR_MESSAGE = + "Registered DMI does not support the ProvMnS interface."; + private final AlternateIdMatcher alternateIdMatcher; private final DmiRestClient dmiRestClient; private final InventoryPersistence inventoryPersistence; + private final ParametersBuilder parametersBuilder; + private final ParameterMapper parameterMapper; + private final ErrorResponseBuilder errorResponseBuilder; private final PolicyExecutor policyExecutor; - private final ProvMnSParametersMapper provMnsParametersMapper; private final JsonObjectMapper jsonObjectMapper; @Override - public ResponseEntity getMoi(final HttpServletRequest httpServletRequest, final Scope scope, + public ResponseEntity getMoi(final HttpServletRequest httpServletRequest, final Scope scope, final String filter, final List attributes, final List fields, final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector) { - final ProvMnsRequestParameters requestParameters = - ProvMnsRequestParameters.extractProvMnsRequestParameters(httpServletRequest); - final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle( - alternateIdMatcher.getCmHandleId(requestParameters.getAlternateId())); - provMnsParametersMapper.checkDataProducerIdentifier(yangModelCmHandle); - final UrlTemplateParameters urlTemplateParameters = provMnsParametersMapper.getUrlTemplateParameters(scope, - filter, attributes, - fields, dataNodeSelector, - yangModelCmHandle); - return dmiRestClient.synchronousGetOperation( - RequiredDmiService.DATA, urlTemplateParameters, OperationType.READ); + final RequestPathParameters requestPathParameters = + parameterMapper.extractRequestParameters(httpServletRequest); + try { + final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle( + alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId( + requestPathParameters.toAlternateId(), "/")); + try { + checkTarget(yangModelCmHandle); + } catch (final ProvMnSException exception) { + final HttpStatus httpStatus = "NOT READY".equals(exception.getMessage()) + ? HttpStatus.NOT_ACCEPTABLE : HttpStatus.UNPROCESSABLE_ENTITY; + return errorResponseBuilder.buildErrorResponseGet(httpStatus, exception.getDetails()); + } + final UrlTemplateParameters urlTemplateParameters = parametersBuilder.createUrlTemplateParametersForGet( + scope, filter, attributes, + fields, dataNodeSelector, + yangModelCmHandle); + return dmiRestClient.synchronousGetOperation( + RequiredDmiService.DATA, urlTemplateParameters, OperationType.READ); + } catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) { + final String reason = buildNotFoundMessage(requestPathParameters.toAlternateId()); + return errorResponseBuilder.buildErrorResponseGet(HttpStatus.NOT_FOUND, reason); + } } @Override - public ResponseEntity patchMoi(final HttpServletRequest httpServletRequest, final Resource resource) { - final ProvMnsRequestParameters requestParameters = - ProvMnsRequestParameters.extractProvMnsRequestParameters(httpServletRequest); - //TODO: implement if a different user sotry - // final ProvMnsRequestParameters requestParameters = - // ProvMnsRequestParameters.extractProvMnsRequestParameters(httpServletRequest); + public ResponseEntity patchMoi(final HttpServletRequest httpServletRequest, final Resource resource) { + final RequestPathParameters requestPathParameters = + parameterMapper.extractRequestParameters(httpServletRequest); + try { + final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle( + alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId( + requestPathParameters.toAlternateId(), "/")); + } catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) { + final String reason = buildNotFoundMessage(requestPathParameters.toAlternateId()); + return errorResponseBuilder.buildErrorResponsePatch(HttpStatus.NOT_FOUND, reason); + } return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } @Override - public ResponseEntity putMoi(final HttpServletRequest httpServletRequest, final Resource resource) { - final ProvMnsRequestParameters provMnsRequestParameters = - ProvMnsRequestParameters.extractProvMnsRequestParameters(httpServletRequest); - return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + public ResponseEntity putMoi(final HttpServletRequest httpServletRequest, final Resource resource) { + final RequestPathParameters requestPathParameters = + parameterMapper.extractRequestParameters(httpServletRequest); + try { + final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle( + alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId( + requestPathParameters.toAlternateId(), "/")); + try { + checkTarget(yangModelCmHandle); + } catch (final ProvMnSException exception) { + final HttpStatus httpStatus = "NOT READY".equals(exception.getMessage()) + ? HttpStatus.NOT_ACCEPTABLE : HttpStatus.UNPROCESSABLE_ENTITY; + return errorResponseBuilder.buildErrorResponseDefault(httpStatus, exception.getDetails()); + } + try { + policyExecutor.checkPermission(yangModelCmHandle, + OperationType.CREATE, + null, + requestPathParameters.toAlternateId(), + jsonObjectMapper.asJsonString(policyExecutor.buildOperationDetails( + OperationType.CREATE, requestPathParameters, resource)) + ); + } catch (final RuntimeException exception) { + return errorResponseBuilder.buildErrorResponseDefault(HttpStatus.NOT_ACCEPTABLE, + exception.getMessage()); + } + final UrlTemplateParameters urlTemplateParameters = + parametersBuilder.createUrlTemplateParametersForPut(resource, + yangModelCmHandle); + return dmiRestClient.synchronousPutOperation( + RequiredDmiService.DATA, urlTemplateParameters, OperationType.CREATE); + } catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) { + final String reason = buildNotFoundMessage(requestPathParameters.toAlternateId()); + return errorResponseBuilder.buildErrorResponseDefault(HttpStatus.NOT_FOUND, reason); + } } @Override public ResponseEntity deleteMoi(final HttpServletRequest httpServletRequest) { - final ProvMnsRequestParameters provMnsRequestParameters = - ProvMnsRequestParameters.extractProvMnsRequestParameters(httpServletRequest); - - final String cmHandleId = alternateIdMatcher.getCmHandleId(provMnsRequestParameters.getFullUriLdn()); - final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle(cmHandleId); - - //TODO: implement if a different user story - //if (!yangModelCmHandle.getDataProducerIdentifier().isEmpty() - // && CmHandleState.READY == yangModelCmHandle.getCompositeState().getCmHandleState()) { - - final ConfigurationManagementDeleteInput configurationManagementDeleteInput = - new ConfigurationManagementDeleteInput(OperationType.DELETE.name(), - provMnsRequestParameters.getFullUriLdn()); + final RequestPathParameters requestPathParameters = + parameterMapper.extractRequestParameters(httpServletRequest); + try { + final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle( + alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId( + requestPathParameters.toAlternateId(), "/")); + try { + checkTarget(yangModelCmHandle); + } catch (final ProvMnSException exception) { + final HttpStatus httpStatus = "NOT READY".equals(exception.getMessage()) + ? HttpStatus.NOT_ACCEPTABLE : HttpStatus.UNPROCESSABLE_ENTITY; + return errorResponseBuilder.buildErrorResponseDefault(httpStatus, exception.getDetails()); + } + try { + policyExecutor.checkPermission(yangModelCmHandle, + OperationType.DELETE, + NO_AUTHORIZATION, + requestPathParameters.toAlternateId(), + jsonObjectMapper.asJsonString(policyExecutor.buildDeleteOperationDetails( + requestPathParameters.toAlternateId())) + ); + } catch (final RuntimeException exception) { + return errorResponseBuilder.buildErrorResponseDefault(HttpStatus.NOT_ACCEPTABLE, + exception.getMessage()); + } + final UrlTemplateParameters urlTemplateParameters = + parametersBuilder.createUrlTemplateParametersForDelete(yangModelCmHandle); + return dmiRestClient.synchronousDeleteOperation(RequiredDmiService.DATA, urlTemplateParameters); + } catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) { + final String reason = buildNotFoundMessage(requestPathParameters.toAlternateId()); + return errorResponseBuilder.buildErrorResponseDefault(HttpStatus.NOT_FOUND, reason); + } + } - policyExecutor.checkPermission(yangModelCmHandle, - OperationType.DELETE, - NO_AUTHORIZATION, - provMnsRequestParameters.getFullUriLdn(), - jsonObjectMapper.asJsonString(configurationManagementDeleteInput)); + private void checkTarget(final YangModelCmHandle yangModelCmHandle) { + if (yangModelCmHandle.getDataProducerIdentifier() == null + || yangModelCmHandle.getDataProducerIdentifier().isBlank()) { + throw new ProvMnSException("NO DATA PRODUCER ID", PROVMNS_NOT_SUPPORTED_ERROR_MESSAGE); + } else if (yangModelCmHandle.getCompositeState().getCmHandleState() != CmHandleState.READY) { + throw new ProvMnSException("NOT READY", buildNotReadyStateMessage(yangModelCmHandle)); + } + } - final UrlTemplateParameters urlTemplateParameters = RestServiceUrlTemplateBuilder.newInstance() - .fixedPathSegment(configurationManagementDeleteInput.targetIdentifier()) - .createUrlTemplateParameters(yangModelCmHandle.getDmiServiceName(), - "/ProvMnS"); + private String buildNotReadyStateMessage(final YangModelCmHandle yangModelCmHandle) { + return yangModelCmHandle.getId() + " is not in ready state. Current state:" + + yangModelCmHandle.getCompositeState().getCmHandleState().name(); + } - return dmiRestClient.synchronousDeleteOperation(RequiredDmiService.DATA, urlTemplateParameters); + private String buildNotFoundMessage(final String alternateId) { + return alternateId + " not found"; } + } diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/ErrorResponseBuilder.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/ErrorResponseBuilder.java new file mode 100644 index 0000000000..8be88d50ea --- /dev/null +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/ErrorResponseBuilder.java @@ -0,0 +1,82 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved. + * ================================================================================ + * 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.rest.provmns; + +import java.util.Map; +import org.onap.cps.ncmp.impl.provmns.model.ErrorResponseDefault; +import org.onap.cps.ncmp.impl.provmns.model.ErrorResponseGet; +import org.onap.cps.ncmp.impl.provmns.model.ErrorResponsePatch; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +@Component +public class ErrorResponseBuilder { + + private static final Map ERROR_MAP = Map.of( + HttpStatus.NOT_FOUND, "IE_NOT_FOUND", + HttpStatus.NOT_ACCEPTABLE, "APPLICATION_LAYER_ERROR", + HttpStatus.UNPROCESSABLE_ENTITY, "SERVER_LIMITATION" + ); + + /** + * Create response entity for default error response. + * Default is used by PUT and DELETE + * + * @param httpStatus HTTP response + * @param reason reason for error response + * @return response entity + */ + public ResponseEntity buildErrorResponseDefault(final HttpStatus httpStatus, final String reason) { + final ErrorResponseDefault errorResponseDefault = new ErrorResponseDefault(ERROR_MAP.get(httpStatus)); + errorResponseDefault.setStatus(httpStatus.toString()); + errorResponseDefault.setReason(reason); + return new ResponseEntity<>(errorResponseDefault, httpStatus); + } + + /** + * Create response entity for get error response. + * + * @param httpStatus HTTP response + * @param reason reason for error response + * @return response entity + */ + public ResponseEntity buildErrorResponseGet(final HttpStatus httpStatus, final String reason) { + final ErrorResponseGet errorResponseGet = new ErrorResponseGet(ERROR_MAP.get(httpStatus)); + errorResponseGet.setStatus(httpStatus.toString()); + errorResponseGet.setReason(reason); + return new ResponseEntity<>(errorResponseGet, httpStatus); + } + + /** + * Create response entity for patch error response. + * + * @param httpStatus HTTP response + * @param reason reason for error response + * @return response entity + */ + public ResponseEntity buildErrorResponsePatch(final HttpStatus httpStatus, final String reason) { + final ErrorResponsePatch errorResponsePatch = new ErrorResponsePatch(ERROR_MAP.get(httpStatus)); + errorResponsePatch.setStatus(httpStatus.toString()); + errorResponsePatch.setReason(reason); + return new ResponseEntity<>(errorResponsePatch, httpStatus); + } +} diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandlerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandlerSpec.groovy index 996d98134c..22d302fb79 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandlerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandlerSpec.groovy @@ -39,7 +39,7 @@ import org.onap.cps.ncmp.impl.data.policyexecutor.PolicyExecutor import org.onap.cps.ncmp.impl.dmi.DmiRestClient import org.onap.cps.ncmp.impl.inventory.InventoryPersistence import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher -import org.onap.cps.ncmp.rest.provmns.exception.InvalidPathException +import org.onap.cps.ncmp.api.exceptions.ProvMnSException import org.onap.cps.ncmp.rest.util.CmHandleStateMapper import org.onap.cps.ncmp.rest.util.DataOperationRequestMapper import org.onap.cps.ncmp.rest.util.DeprecationHelper @@ -48,7 +48,7 @@ import org.onap.cps.api.exceptions.AlreadyDefinedException import org.onap.cps.api.exceptions.CpsException import org.onap.cps.api.exceptions.DataNodeNotFoundException import org.onap.cps.api.exceptions.DataValidationException -import org.onap.cps.ncmp.rest.util.ProvMnSParametersMapper +import org.onap.cps.ncmp.impl.provmns.ParametersBuilder import org.onap.cps.ncmp.rest.util.RestOutputCmHandleMapper import org.onap.cps.utils.JsonObjectMapper import org.spockframework.spring.SpringBean @@ -116,7 +116,10 @@ class NetworkCmProxyRestExceptionHandlerSpec extends Specification { RestOutputCmHandleMapper mockRestOutputCmHandleMapper = Mock() @SpringBean - ProvMnSParametersMapper provMnSParametersMapper = Mock() + ParametersBuilder mockProvMnSParametersMapper = Mock() + + @SpringBean + ProvMnsController mockProvMnsController = Mock() @SpringBean AlternateIdMatcher alternateIdMatcher = Mock() @@ -153,20 +156,21 @@ class NetworkCmProxyRestExceptionHandlerSpec extends Specification { then: 'an HTTP response is returned with correct message and details' assertTestResponse(response, expectedErrorCode, expectedErrorMessage, expectedErrorDetails) where: - scenario | exception || expectedErrorCode | expectedErrorMessage | expectedErrorDetails - 'CPS' | new CpsException(sampleErrorMessage, sampleErrorDetails) || INTERNAL_SERVER_ERROR | sampleErrorMessage | sampleErrorDetails - 'NCMP-server' | new ServerNcmpException(sampleErrorMessage, sampleErrorDetails) || INTERNAL_SERVER_ERROR | sampleErrorMessage | null - 'DMI Request' | new DmiRequestException(sampleErrorMessage, sampleErrorDetails) || BAD_REQUEST | sampleErrorMessage | null - 'Invalid Operation' | new InvalidOperationException('some reason') || BAD_REQUEST | 'some reason' | null - 'Unsupported Operation' | new OperationNotSupportedException('not yet') || BAD_REQUEST | 'not yet' | null - 'DataNode Validation' | new DataNodeNotFoundException('myDataspaceName', 'myAnchorName') || NOT_FOUND | 'DataNode not found' | null - 'other' | new IllegalStateException(sampleErrorMessage) || INTERNAL_SERVER_ERROR | sampleErrorMessage | null - 'Data Node Not Found' | new DataNodeNotFoundException('myDataspaceName', 'myAnchorName') || NOT_FOUND | 'DataNode not found' | 'DataNode not found' - 'Existing entry' | new AlreadyDefinedException('name',null) || CONFLICT | 'Already defined exception' | 'name already exists' - 'Existing entries' | AlreadyDefinedException.forDataNodes(['A', 'B'], 'myAnchorName') || CONFLICT | 'Already defined exception' | '2 data node(s) already exist' - 'Operation too large' | new PayloadTooLargeException(sampleErrorMessage) || PAYLOAD_TOO_LARGE | sampleErrorMessage | 'Check logs' - 'Policy Executor' | new PolicyExecutorException(sampleErrorMessage, sampleErrorDetails, null) || CONFLICT | sampleErrorMessage | sampleErrorDetails - 'Invalid Path' | new InvalidPathException("some invalid path") || UNPROCESSABLE_ENTITY | 'not a valid path' | 'some invalid path not a valid path' + scenario | exception || expectedErrorCode | expectedErrorMessage | expectedErrorDetails + 'CPS' | new CpsException(sampleErrorMessage, sampleErrorDetails) || INTERNAL_SERVER_ERROR | sampleErrorMessage | sampleErrorDetails + 'NCMP-server' | new ServerNcmpException(sampleErrorMessage, sampleErrorDetails) || INTERNAL_SERVER_ERROR | sampleErrorMessage | null + 'DMI Request' | new DmiRequestException(sampleErrorMessage, sampleErrorDetails) || BAD_REQUEST | sampleErrorMessage | null + 'Invalid Operation' | new InvalidOperationException('some reason') || BAD_REQUEST | 'some reason' | null + 'Unsupported Operation' | new OperationNotSupportedException('not yet') || BAD_REQUEST | 'not yet' | null + 'DataNode Validation' | new DataNodeNotFoundException('myDataspaceName', 'myAnchorName') || NOT_FOUND | 'DataNode not found' | null + 'other' | new IllegalStateException(sampleErrorMessage) || INTERNAL_SERVER_ERROR | sampleErrorMessage | null + 'Data Node Not Found' | new DataNodeNotFoundException('myDataspaceName', 'myAnchorName') || NOT_FOUND | 'DataNode not found' | 'DataNode not found' + 'Existing entry' | new AlreadyDefinedException('name',null) || CONFLICT | 'Already defined exception' | 'name already exists' + 'Existing entries' | AlreadyDefinedException.forDataNodes(['A', 'B'], 'myAnchorName') || CONFLICT | 'Already defined exception' | '2 data node(s) already exist' + 'Operation too large' | new PayloadTooLargeException(sampleErrorMessage) || PAYLOAD_TOO_LARGE | sampleErrorMessage | 'Check logs' + 'Policy Executor' | new PolicyExecutorException(sampleErrorMessage, sampleErrorDetails, null) || CONFLICT | sampleErrorMessage | sampleErrorDetails + 'Invalid Path' | new ProvMnSException('not a valid path' ,'some invalid path not a valid path') || UNPROCESSABLE_ENTITY | 'not a valid path' | 'some invalid path not a valid path' + 'Invalid Path' | new ProvMnSException('some invalid path not a valid path') || UNPROCESSABLE_ENTITY | 'some invalid path not a valid path' | null } def 'Post request with exception returns correct HTTP Status.'() { diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnsControllerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnsControllerSpec.groovy index e4050c541e..a858f3d98a 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnsControllerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnsControllerSpec.groovy @@ -22,37 +22,40 @@ package org.onap.cps.ncmp.rest.controller import com.fasterxml.jackson.databind.ObjectMapper import jakarta.servlet.ServletException -import org.onap.cps.ncmp.api.data.models.OperationType -import org.onap.cps.ncmp.api.inventory.models.CompositeStateBuilder +import org.onap.cps.ncmp.api.inventory.models.CompositeState +import org.onap.cps.ncmp.exceptions.NoAlternateIdMatchFoundException import org.onap.cps.ncmp.impl.data.policyexecutor.PolicyExecutor import org.onap.cps.ncmp.impl.dmi.DmiRestClient import org.onap.cps.ncmp.impl.inventory.InventoryPersistence import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle -import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher import org.onap.cps.ncmp.impl.provmns.model.ResourceOneOf -import org.onap.cps.ncmp.rest.util.ProvMnSParametersMapper +import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher +import org.onap.cps.ncmp.rest.provmns.ErrorResponseBuilder +import org.onap.cps.ncmp.impl.provmns.ParametersBuilder +import org.onap.cps.ncmp.impl.provmns.ParameterMapper import org.onap.cps.utils.JsonObjectMapper import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.http.HttpStatus -import org.springframework.http.HttpStatusCode import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.test.web.servlet.MockMvc import spock.lang.Specification import static org.onap.cps.ncmp.api.inventory.models.CmHandleState.READY +import static org.onap.cps.ncmp.api.inventory.models.CmHandleState.ADVISED import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put @WebMvcTest(ProvMnsController) class ProvMnsControllerSpec extends Specification { @SpringBean - ProvMnSParametersMapper provMnSParametersMapper = new ProvMnSParametersMapper() + ParametersBuilder parametersBuilder = new ParametersBuilder() @SpringBean AlternateIdMatcher alternateIdMatcher = Mock() @@ -61,10 +64,16 @@ class ProvMnsControllerSpec extends Specification { InventoryPersistence inventoryPersistence = Mock() @SpringBean - PolicyExecutor policyExecutor = Mock() + DmiRestClient dmiRestClient = Mock() @SpringBean - DmiRestClient dmiRestClient = Mock() + ErrorResponseBuilder errorResponseBuilder = new ErrorResponseBuilder() + + @SpringBean + ParameterMapper parameterMapper = new ParameterMapper() + + @SpringBean + PolicyExecutor policyExecutor = Mock() @Autowired MockMvc mvc @@ -72,31 +81,48 @@ class ProvMnsControllerSpec extends Specification { @SpringBean JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) + def jsonBody = jsonObjectMapper.asJsonString(new ResourceOneOf('test')) + @Value('${rest.api.provmns-base-path}') def provMnSBasePath - def 'Get Resource Data from provmns interface #scenario.'() { + def 'Get resource data request where #scenario'() { given: 'resource data url' - def getUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId"+path - and: 'request classes return correct information' - inventoryPersistence.getYangModelCmHandle("cm-1") >> new YangModelCmHandle(dmiServiceName: "someDmiService", dataProducerIdentifier: 'someUriLdnFirstPart/someClassName=someId') - alternateIdMatcher.getCmHandleId("someUriLdnFirstPart/someClassName=someId") >> "cm-1" - dmiRestClient.synchronousGetOperation(*_) >> new ResponseEntity(HttpStatusCode.valueOf(200)) + def getUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId?attributes=[test,query,param]" + and: 'alternate Id can be matched' + alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/someUriLdnFirstPart/someClassName=someId', "/") >> 'cm-1' + and: 'persistence service returns yangModelCmHandle' + inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: dataProducerId, compositeState: new CompositeState(cmHandleState: state)) + and: 'dmi provides a response' + dmiRestClient.synchronousGetOperation(*_) >> new ResponseEntity<>('Some response from DMI service', HttpStatus.OK) when: 'get data resource request is performed' def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response - then: 'response status is OK (200)' - assert response.status == HttpStatus.OK.value() + then: 'response status as expected' + assert response.status == expectedHttpStatus.value() where: - scenario | path - 'with no query params' | '' - 'with query params' | '?attributes=[test,query,param]' + scenario | dataProducerId | state || expectedHttpStatus + 'cmHandle state is Ready with populated dataProducerId' | 'someDataProducerId' | READY || HttpStatus.OK + 'dataProducerId is empty' | '' | READY || HttpStatus.UNPROCESSABLE_ENTITY + 'dataProducerId is null' | null | READY || HttpStatus.UNPROCESSABLE_ENTITY + 'cmHandle state is Advised' | 'someDataProducerId' | ADVISED || HttpStatus.NOT_ACCEPTABLE + } + + def 'Get resource data request with no match for alternate id'() { + given: 'resource data url' + def getUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId" + and: 'alternate Id cannot be matched' + alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(*_) >> {throw new NoAlternateIdMatchFoundException('someUriLdnFirstPart/someClassName=someId')} + and: 'persistence service returns yangModelCmHandle' + inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: ADVISED)) + when: 'get data resource request is performed' + def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response + then: 'response status is NOT_FOUND (404)' + assert response.status == HttpStatus.NOT_FOUND.value() } def 'Patch Resource Data from provmns interface.'() { given: 'resource data url' def patchUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId" - and: 'an example resource json object' - def jsonBody = jsonObjectMapper.asJsonString(new ResourceOneOf('test')) when: 'patch data resource request is performed' def response = mvc.perform(patch(patchUrl) .contentType(new MediaType('application', 'json-patch+json')) @@ -106,42 +132,134 @@ class ProvMnsControllerSpec extends Specification { assert response.status == HttpStatus.NOT_IMPLEMENTED.value() } - def 'Put Resource Data from provmns interface.'() { + def 'Patch resource data request with no match for alternate id'() { given: 'resource data url' - def putUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId" - and: 'a valid json body containing a valid Resource instance' - def jsonBody = '{ "id": "some-resource" }' + def patchUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId" + and: 'alternate Id cannot be matched' + alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(*_) >> {throw new NoAlternateIdMatchFoundException('/someUriLdnFirstPart/someClassName=someId')} + and: 'persistence service returns valid yangModelCmHandle' + inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: ADVISED)) when: 'patch data resource request is performed' - def response = mvc.perform(patch(putUrl) - .contentType(new MediaType('application', 'json-patch+json')) + def response = mvc.perform(patch(patchUrl) + .contentType("application/json-patch+json") .content(jsonBody)) .andReturn().response - then: 'response status is Not Implemented (501)' - assert response.status == HttpStatus.NOT_IMPLEMENTED.value() + then: 'response status is NOT_FOUND (404)' + assert response.status == HttpStatus.NOT_FOUND.value() + } + + def 'Put resource data request where #scenario'() { + given: 'resource data url' + def putUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId" + and: 'alternate Id can be matched' + alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/someUriLdnFirstPart/someClassName=someId', "/") >> 'cm-1' + and: 'persistence service returns yangModelCmHandle' + inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: dataProducerId, compositeState: new CompositeState(cmHandleState: state)) + and: 'dmi provides a response' + dmiRestClient.synchronousPutOperation(*_) >> new ResponseEntity<>('Some response from DMI service', HttpStatus.OK) + when: 'put data resource request is performed' + def response = mvc.perform(put(putUrl) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonBody)) + .andReturn().response + then: 'response status is as expected' + assert response.status == expectedHttpStatus.value() + where: + scenario | dataProducerId | state || expectedHttpStatus + 'valid request is made' | 'someDataProducerId' | READY || HttpStatus.OK + 'dataProducerId is empty' | '' | READY || HttpStatus.UNPROCESSABLE_ENTITY + 'dataProducerId is null' | null | READY || HttpStatus.UNPROCESSABLE_ENTITY + 'cmHandle state is Advised' | 'someDataProducerId' | ADVISED || HttpStatus.NOT_ACCEPTABLE + } + + def 'Put resource data request with no match for alternate id'() { + given: 'resource data url' + def putUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId" + and: 'alternate Id cannot be matched' + alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(*_) >> {throw new NoAlternateIdMatchFoundException('someUriLdnFirstPart/someClassName=someId')} + and: 'persistence service returns valid yangModelCmHandle' + inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: READY)) + when: 'put data resource request is performed' + def response = mvc.perform(put(putUrl) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonBody)) + .andReturn().response + then: 'response status is NOT_FOUND (404)' + assert response.status == HttpStatus.NOT_FOUND.value() + } + + def 'Put resource data request with no permission from coordination management'() { + given: 'resource data url' + def putUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId" + and: 'alternate Id can be matched' + alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/someUriLdnFirstPart/someClassName=someId', "/") >> 'cm-1' + and: 'persistence service returns valid yangModelCmHandle' + inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: READY)) + and: 'policy executor throws exception (denied)' + policyExecutor.checkPermission(*_) >> {throw new RuntimeException()} + when: 'put data resource request is performed' + def response = mvc.perform(put(putUrl) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonBody)) + .andReturn().response + then: 'response status is NOT_ACCEPTABLE (406)' + assert response.status == HttpStatus.NOT_ACCEPTABLE.value() + } + + def 'Delete resource data request where #scenario'() { + given: 'resource data url' + def deleteUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId" + and: 'alternate Id can be matched' + alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/someUriLdnFirstPart/someClassName=someId', "/") >> 'cm-1' + and: 'persistence service returns yangModelCmHandle' + inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: dataProducerId, compositeState: new CompositeState(cmHandleState: state)) + and: 'dmi provides a response' + dmiRestClient.synchronousDeleteOperation(*_) >> new ResponseEntity<>('Some response from DMI service', HttpStatus.OK) + when: 'get data resource request is performed' + def response = mvc.perform(delete(deleteUrl)).andReturn().response + then: 'response status as expected' + assert response.status == expectedHttpStatus.value() + where: + scenario | dataProducerId | state || expectedHttpStatus + 'valid request is made' | 'someDataProducerId' | READY || HttpStatus.OK + 'dataProducerId is empty' | '' | READY || HttpStatus.UNPROCESSABLE_ENTITY + 'dataProducerId is null' | null | READY || HttpStatus.UNPROCESSABLE_ENTITY + 'cmHandle state is Advised' | 'someDataProducerId' | ADVISED || HttpStatus.NOT_ACCEPTABLE + } + + def 'Delete resource data request with no match for alternate id'() { + given: 'resource data url' + def deleteUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId" + and: 'alternate Id cannot be matched' + alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(*_) >> {throw new NoAlternateIdMatchFoundException('someUriLdnFirstPart/someClassName=someId')} + and: 'persistence service returns valid yangModelCmHandle' + inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: READY)) + when: 'delete data resource request is performed' + def response = mvc.perform(delete(deleteUrl)).andReturn().response + then: 'response status is NOT_FOUND (404)' + assert response.status == HttpStatus.NOT_FOUND.value() } - def 'Delete Resource Data from provmns interface.'() { + def 'Delete resource data request with no permission from coordination management'() { given: 'resource data url' - def deleteUrl = "$provMnSBasePath/v1/someLdnFirstPart/someClass=someId" - def readyState = new CompositeStateBuilder().withCmHandleState(READY).withLastUpdatedTimeNow().build() - and: 'a cm handle found by alternate id and dmi returns a response' - alternateIdMatcher.getCmHandleId("/someLdnFirstPart/someClass=someId") >> "cm-1" - inventoryPersistence.getYangModelCmHandle("cm-1") >> new YangModelCmHandle(dmiServiceName: 'sampleDmiService', dataProducerIdentifier: 'some-producer', compositeState: readyState) - dmiRestClient.synchronousDeleteOperation(*_) >> new ResponseEntity<>('Response from DMI service', HttpStatus.I_AM_A_TEAPOT) - and: 'the policy executor invoked' - 1 * policyExecutor.checkPermission(_, OperationType.DELETE, null, '/someLdnFirstPart/someClass=someId', _) + def deleteUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId" + and: 'alternate Id can be matched' + alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId('/someUriLdnFirstPart/someClassName=someId', "/") >> 'cm-1' + and: 'persistence service returns valid yangModelCmHandle' + inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: READY)) + and: 'policy executor throws exception (denied)' + policyExecutor.checkPermission(*_) >> {throw new RuntimeException()} when: 'delete data resource request is performed' - def result = mvc.perform(delete(deleteUrl)).andReturn().response - then: 'result status and content equals the response from the DMI' - assert result.status == HttpStatus.I_AM_A_TEAPOT.value() - assert result.contentAsString == 'Response from DMI service' + def response = mvc.perform(delete(deleteUrl)).andReturn().response + then: 'response status is NOT_ACCEPTABLE (406)' + assert response.status == HttpStatus.NOT_ACCEPTABLE.value() } def 'Invalid path passed in to provmns interface, #scenario'() { given: 'an invalid path' def url = "$provMnSBasePath/v1/" + invalidPath when: 'get data resource request is performed' - mvc.perform(get(url).contentType(MediaType.APPLICATION_JSON)) + def response = mvc.perform(get(url).contentType(MediaType.APPLICATION_JSON)).andReturn().response then: 'invalid path exception is thrown' thrown(ServletException) where: diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/util/ProvMnSParametersMapperSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/util/ProvMnSParametersMapperSpec.groovy deleted file mode 100644 index ee8a961838..0000000000 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/util/ProvMnSParametersMapperSpec.groovy +++ /dev/null @@ -1,63 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved. - * ================================================================================ - * 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.rest.util - -import org.onap.cps.ncmp.api.exceptions.NcmpException -import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle -import org.onap.cps.ncmp.impl.provmns.model.ClassNameIdGetDataNodeSelectorParameter -import org.onap.cps.ncmp.impl.provmns.model.Scope -import spock.lang.Specification - -class ProvMnSParametersMapperSpec extends Specification{ - - def objectUnderTest = new ProvMnSParametersMapper() - - def 'Extract url template parameters for GET'() { - when:'a set of given parameters from a call are passed in' - def result = objectUnderTest.getUrlTemplateParameters(new Scope(scopeLevel: 1, scopeType: 'BASE_ALL'), - 'some-filter', ['some-attribute'], ['some-field'], new ClassNameIdGetDataNodeSelectorParameter(dataNodeSelector: 'some-dataSelector'), - new YangModelCmHandle(dmiServiceName: 'some-dmi-service')) - then:'verify object has been mapped correctly' - result.urlVariables().get('filter') == 'some-filter' - } - - def 'Data Producer Identifier validation.'() { - given:'a yangModelCmHandle' - def yangModelCmHandle = new YangModelCmHandle(dataProducerIdentifier: 'some-dataProducer-ID') - when:'a yangModelCmHandle is passed in' - def result = objectUnderTest.checkDataProducerIdentifier(yangModelCmHandle) - then: 'no exception thrown for yangModelCmHandle when a data producer is present' - noExceptionThrown() - } - - def 'Data Producer Identifier validation with #scenario.'() { - given:'a yangModelCmHandle' - def yangModelCmHandle = new YangModelCmHandle(dataProducerIdentifier: dataProducerId) - when:'a data producer identifier is checked' - def result = objectUnderTest.checkDataProducerIdentifier(yangModelCmHandle) - then: 'exception thrown' - thrown(NcmpException) - where: - scenario | dataProducerId - 'null' | null - 'blank' | '' - - } -} diff --git a/cps-ncmp-service/pom.xml b/cps-ncmp-service/pom.xml index 756b9970dc..3bffd573e4 100644 --- a/cps-ncmp-service/pom.xml +++ b/cps-ncmp-service/pom.xml @@ -34,7 +34,7 @@ cps-ncmp-service - 0.98 + 0.96 diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/exception/InvalidPathException.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/exceptions/ProvMnSException.java similarity index 68% rename from cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/exception/InvalidPathException.java rename to cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/exceptions/ProvMnSException.java index f9cc5cca57..cf9fca7b83 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/exception/InvalidPathException.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/exceptions/ProvMnSException.java @@ -18,23 +18,27 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.ncmp.rest.provmns.exception; +package org.onap.cps.ncmp.api.exceptions; -import org.onap.cps.ncmp.api.exceptions.NcmpException; +public class ProvMnSException extends NcmpException { -public class InvalidPathException extends NcmpException { - - private static final String INVALID_PATH_DETAILS_FORMAT = - "%s not a valid path"; + /** + * Constructor. + * + * @param message exception message + * @param details exception details + */ + public ProvMnSException(final String message, final String details) { + super(message, details); + } /** * Constructor. * - * @param path provmns uri path + * @param message exception message */ - public InvalidPathException(final String path) { - super("not a valid path", String.format(INVALID_PATH_DETAILS_FORMAT, - path)); + public ProvMnSException(final String message) { + super(message, null); } } diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/model/ConfigurationManagementDeleteInput.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/DeleteOperationDetails.java similarity index 86% rename from cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/model/ConfigurationManagementDeleteInput.java rename to cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/DeleteOperationDetails.java index 55494c88de..b24583dcf9 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/model/ConfigurationManagementDeleteInput.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/DeleteOperationDetails.java @@ -18,6 +18,6 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.ncmp.rest.provmns.model; +package org.onap.cps.ncmp.impl.data.policyexecutor; -public record ConfigurationManagementDeleteInput (String operationType, String targetIdentifier) {} \ No newline at end of file +public record DeleteOperationDetails(String operationType, String targetIdentifier) {} \ No newline at end of file diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetails.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetails.java new file mode 100644 index 0000000000..16a2918907 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetails.java @@ -0,0 +1,30 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 OpenInfra Foundation Europe + * ================================================================================ + * 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.impl.data.policyexecutor; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.List; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record OperationDetails(String operation, + String targetIdentifier, + Map> changeRequest) {} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationEntry.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationEntry.java new file mode 100644 index 0000000000..542a6b7692 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationEntry.java @@ -0,0 +1,38 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 OpenInfra Foundation Europe + * ================================================================================ + * 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.impl.data.policyexecutor; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.Setter; + +/** + * Represents a single managed object included in a change request, + * containing its identifier and arbitrary attributes. + */ +@Setter +@Getter +@JsonInclude(JsonInclude.Include.NON_NULL) +public class OperationEntry { + private String id; + private Object attributes; + +} \ No newline at end of file diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutor.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutor.java index e9d83aaa47..8dd9881b09 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutor.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutor.java @@ -21,6 +21,7 @@ package org.onap.cps.ncmp.impl.data.policyexecutor; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.UnknownHostException; @@ -29,6 +30,7 @@ import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeoutException; import lombok.RequiredArgsConstructor; @@ -37,8 +39,11 @@ import org.onap.cps.ncmp.api.data.models.OperationType; import org.onap.cps.ncmp.api.exceptions.NcmpException; import org.onap.cps.ncmp.api.exceptions.PolicyExecutorException; import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle; +import org.onap.cps.ncmp.impl.provmns.RequestPathParameters; +import org.onap.cps.ncmp.impl.provmns.model.Resource; import org.onap.cps.ncmp.impl.utils.http.RestServiceUrlTemplateBuilder; import org.onap.cps.ncmp.impl.utils.http.UrlTemplateParameters; +import org.onap.cps.utils.JsonObjectMapper; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; @@ -78,6 +83,7 @@ public class PolicyExecutor { private final WebClient policyExecutorWebClient; private final ObjectMapper objectMapper; + private final JsonObjectMapper jsonObjectMapper; private static final Throwable NO_ERROR = null; @@ -116,6 +122,48 @@ public class PolicyExecutor { } } + /** + * Build a OperationDetails object from ProvMnS request details. + * + * @param operationType Type of operation delete, create etc. + * @param requestPathParameters request parameters including uri-ldn-first-part, className and id + * @param resource provided request resource + * @return OperationDetails object + */ + public OperationDetails buildOperationDetails(final OperationType operationType, + final RequestPathParameters requestPathParameters, + final Resource resource) { + final Map> changeRequest = new HashMap<>(); + final OperationEntry operationEntry = new OperationEntry(); + + final String resourceJson = jsonObjectMapper.asJsonString(resource); + String className = requestPathParameters.getClassName(); + try { + final TypeReference> typeReference = + new TypeReference>() {}; + final Map fullValue = objectMapper.readValue(resourceJson, typeReference); + + operationEntry.setId(requestPathParameters.getId()); + operationEntry.setAttributes(fullValue.get("attributes")); + className = isNullEmptyOrBlank(fullValue) + ? requestPathParameters.getClassName() : fullValue.get("objectClass").toString(); + } catch (final JsonProcessingException exception) { + log.debug("JSON processing error: {}", exception); + } + changeRequest.put(className, List.of(operationEntry)); + return new OperationDetails(operationType.name(), requestPathParameters.toAlternateId(), changeRequest); + } + + /** + * Builds a DeleteOperationDetails object from provided alternate id. + * + * @param alternateId alternate id for request + * @return DeleteOperationDetails object + */ + public DeleteOperationDetails buildDeleteOperationDetails(final String alternateId) { + return new DeleteOperationDetails(OperationType.DELETE.name(), alternateId); + } + private Map getSingleOperationAsMap(final YangModelCmHandle yangModelCmHandle, final OperationType operationType, final String resourceIdentifier, @@ -241,4 +289,12 @@ public class PolicyExecutor { log.warn(warning); processDecision(decisionId, decision, warning, cause); } + + private boolean isNullEmptyOrBlank(final Map jsonObject) { + try { + return jsonObject.get("objectClass").toString().isBlank(); + } catch (final NullPointerException exception) { + return true; + } + } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/dmi/DmiRestClient.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/dmi/DmiRestClient.java index a2558d144a..cec1c3683f 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/dmi/DmiRestClient.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/dmi/DmiRestClient.java @@ -34,7 +34,6 @@ import lombok.extern.slf4j.Slf4j; import org.onap.cps.ncmp.api.data.models.OperationType; import org.onap.cps.ncmp.api.exceptions.DmiClientRequestException; import org.onap.cps.ncmp.impl.models.RequiredDmiService; -import org.onap.cps.ncmp.impl.provmns.model.Resource; import org.onap.cps.ncmp.impl.utils.http.UrlTemplateParameters; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.beans.factory.annotation.Qualifier; @@ -129,7 +128,7 @@ public class DmiRestClient { * @return ResponseEntity containing the response from the DMI. * @throws DmiClientRequestException If there is an error during the DMI request. */ - public ResponseEntity synchronousGetOperation(final RequiredDmiService requiredDmiService, + public ResponseEntity synchronousGetOperation(final RequiredDmiService requiredDmiService, final UrlTemplateParameters urlTemplateParameters, final OperationType operationType) { @@ -138,7 +137,30 @@ public class DmiRestClient { .uri(urlTemplateParameters.urlTemplate(), urlTemplateParameters.urlVariables()) .headers(httpHeaders -> configureHttpHeaders(httpHeaders, NO_AUTHORIZATION)) .retrieve() - .toEntity(Resource.class) + .toEntity(Object.class) + .onErrorMap(throwable -> handleDmiClientException(throwable, operationType.getOperationName())) + .block(); + } + + /** + * Sends a synchronous (blocking) PUT operation to the DMI. + * + * @param requiredDmiService Determines if the required service is for a data or model operation. + * @param urlTemplateParameters The DMI resource URL template with variables. + * @param operationType The type of operation being executed (for error reporting only). + * @return ResponseEntity containing the response from the DMI. + * @throws DmiClientRequestException If there is an error during the DMI request. + */ + public ResponseEntity synchronousPutOperation(final RequiredDmiService requiredDmiService, + final UrlTemplateParameters + urlTemplateParameters, + final OperationType operationType) { + return getWebClient(requiredDmiService) + .get() + .uri(urlTemplateParameters.urlTemplate(), urlTemplateParameters.urlVariables()) + .headers(httpHeaders -> configureHttpHeaders(httpHeaders, NO_AUTHORIZATION)) + .retrieve() + .toEntity(Object.class) .onErrorMap(throwable -> handleDmiClientException(throwable, operationType.getOperationName())) .block(); } diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/ProvMnsRequestParameters.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterMapper.java similarity index 53% rename from cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/ProvMnsRequestParameters.java rename to cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterMapper.java index 89a5301e96..b58fe34852 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/ProvMnsRequestParameters.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParameterMapper.java @@ -18,61 +18,46 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.ncmp.rest.util; +package org.onap.cps.ncmp.impl.provmns; import jakarta.servlet.http.HttpServletRequest; -import lombok.Getter; -import lombok.Setter; -import org.onap.cps.ncmp.rest.provmns.exception.InvalidPathException; +import lombok.RequiredArgsConstructor; +import org.onap.cps.ncmp.api.exceptions.ProvMnSException; +import org.springframework.stereotype.Service; -@Getter -@Setter -public class ProvMnsRequestParameters { - - private String fullUriLdn; - private String uriLdnFirstPart; - private String className; - private String id; +@Service +@RequiredArgsConstructor +public class ParameterMapper { private static final String PROVMNS_BASE_PATH = "ProvMnS/v\\d+/"; + private static final String INVALID_PATH_DETAILS_FORMAT = "%s not a valid path"; /** - * Gets alternate id from combining URI-LDN-First-Part, className and Id. - * - * @return String of Alternate Id. - */ - public String getAlternateId() { - return uriLdnFirstPart + "/" + className + "=" + id; - } - - /** - * Converts HttpServletRequest to ProvMnsRequestParameters. + * Converts HttpServletRequest to RequestPathParameters. * * @param httpServletRequest HttpServletRequest object containing the path - * @return ProvMnsRequestParameters object containing parsed parameters + * @return RequestPathParameters object containing parsed parameters */ - public static ProvMnsRequestParameters extractProvMnsRequestParameters( - final HttpServletRequest httpServletRequest) { + public RequestPathParameters extractRequestParameters(final HttpServletRequest httpServletRequest) { final String uriPath = (String) httpServletRequest.getAttribute( "org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping"); final String[] pathVariables = uriPath.split(PROVMNS_BASE_PATH); final int lastSlashIndex = pathVariables[1].lastIndexOf('/'); - if (lastSlashIndex == -1) { - throw new InvalidPathException(uriPath); + if (lastSlashIndex < 0) { + throw new ProvMnSException("not a valid path", String.format(INVALID_PATH_DETAILS_FORMAT, uriPath)); } - final ProvMnsRequestParameters provMnsRequestParameters = new ProvMnsRequestParameters(); - provMnsRequestParameters.setFullUriLdn("/" + pathVariables[1]); - provMnsRequestParameters.setUriLdnFirstPart(pathVariables[1].substring(0, lastSlashIndex)); + final RequestPathParameters requestPathParameters = new RequestPathParameters(); + requestPathParameters.setUriLdnFirstPart("/" + pathVariables[1].substring(0, lastSlashIndex)); final String classNameAndId = pathVariables[1].substring(lastSlashIndex + 1); final String[] splitClassNameId = classNameAndId.split("=", 2); if (splitClassNameId.length != 2) { - throw new InvalidPathException(uriPath); + throw new ProvMnSException("not a valid path", String.format(INVALID_PATH_DETAILS_FORMAT, uriPath)); } - provMnsRequestParameters.setClassName(splitClassNameId[0]); - provMnsRequestParameters.setId(splitClassNameId[1]); + requestPathParameters.setClassName(splitClassNameId[0]); + requestPathParameters.setId(splitClassNameId[1]); - return provMnsRequestParameters; + return requestPathParameters; } } diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/ProvMnSParametersMapper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParametersBuilder.java similarity index 56% rename from cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/ProvMnSParametersMapper.java rename to cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParametersBuilder.java index 1f7c707a2c..cf85d740e3 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/util/ProvMnSParametersMapper.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParametersBuilder.java @@ -18,14 +18,13 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.ncmp.rest.util; +package org.onap.cps.ncmp.impl.provmns; import java.util.List; import lombok.RequiredArgsConstructor; -import org.onap.cps.ncmp.api.exceptions.NcmpException; -import org.onap.cps.ncmp.impl.dmi.DmiServiceAuthenticationProperties; import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle; import org.onap.cps.ncmp.impl.provmns.model.ClassNameIdGetDataNodeSelectorParameter; +import org.onap.cps.ncmp.impl.provmns.model.Resource; import org.onap.cps.ncmp.impl.provmns.model.Scope; import org.onap.cps.ncmp.impl.utils.http.RestServiceUrlTemplateBuilder; import org.onap.cps.ncmp.impl.utils.http.UrlTemplateParameters; @@ -33,9 +32,7 @@ import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor -public class ProvMnSParametersMapper { - - private final DmiServiceAuthenticationProperties dmiServiceAuthenticationProperties; +public class ParametersBuilder { /** * Creates a UrlTemplateParameters object containing the relevant fields for a get. @@ -48,12 +45,13 @@ public class ProvMnSParametersMapper { * @param yangModelCmHandle yangModelCmHandle object for resolved alternate ID * @return UrlTemplateParameters object. */ - public UrlTemplateParameters getUrlTemplateParameters(final Scope scope, final String filter, - final List attributes, final List fields, - final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector, - final YangModelCmHandle yangModelCmHandle) { - + public UrlTemplateParameters createUrlTemplateParametersForGet(final Scope scope, final String filter, + final List attributes, + final List fields, + final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector, + final YangModelCmHandle yangModelCmHandle) { return RestServiceUrlTemplateBuilder.newInstance() + .fixedPathSegment(yangModelCmHandle.getAlternateId()) .queryParameter("scopeType", scope.getScopeType() != null ? scope.getScopeType().getValue() : null) .queryParameter("scopeLevel", scope.getScopeLevel() != null @@ -61,21 +59,36 @@ public class ProvMnSParametersMapper { .queryParameter("filter", filter) .queryParameter("attributes", attributes != null ? attributes.toString() : null) .queryParameter("fields", fields != null ? fields.toString() : null) - .queryParameter("dataNodeSelector", dataNodeSelector.getDataNodeSelector() != null - ? dataNodeSelector.getDataNodeSelector() : null) + .queryParameter("dataNodeSelector", dataNodeSelector.getDataNodeSelector()) .createUrlTemplateParameters(yangModelCmHandle.getDmiServiceName(), "ProvMnS"); } /** - * Check if dataProducerIdentifier is empty or null, if so throw exception. + * Creates a UrlTemplateParameters object containing the relevant fields for a put. * - * @param yangModelCmHandle given yangModelCmHandle. + * @param resource Provided resource parameter. + * @param yangModelCmHandle yangModelCmHandle object for resolved alternate ID + * @return UrlTemplateParameters object. */ - public void checkDataProducerIdentifier(final YangModelCmHandle yangModelCmHandle) { - if (yangModelCmHandle.getDataProducerIdentifier() == null - || yangModelCmHandle.getDataProducerIdentifier().isEmpty()) { - throw new NcmpException("No data producer identifier registered for cm handle", - "Cm Handle " + yangModelCmHandle.getId() + " has empty data producer identifier"); - } + public UrlTemplateParameters createUrlTemplateParametersForPut(final Resource resource, + final YangModelCmHandle yangModelCmHandle) { + + return RestServiceUrlTemplateBuilder.newInstance() + .fixedPathSegment(yangModelCmHandle.getAlternateId()) + .queryParameter("resource", resource.toString()) + .createUrlTemplateParameters(yangModelCmHandle.getDmiServiceName(), "ProvMnS"); + } + + /** + * Creates a UrlTemplateParameters object containing the relevant fields for a delete action. + * + * @param yangModelCmHandle yangModelCmHandle object for resolved alternate ID + * @return UrlTemplateParameters object. + */ + public UrlTemplateParameters createUrlTemplateParametersForDelete(final YangModelCmHandle yangModelCmHandle) { + + return RestServiceUrlTemplateBuilder.newInstance() + .fixedPathSegment(yangModelCmHandle.getAlternateId()) + .createUrlTemplateParameters(yangModelCmHandle.getDmiServiceName(), "ProvMnS"); } -} +} \ No newline at end of file diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/RequestPathParameters.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/RequestPathParameters.java new file mode 100644 index 0000000000..956b190d2a --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/RequestPathParameters.java @@ -0,0 +1,42 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 OpenInfra Foundation Europe + * ================================================================================ + * 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.impl.provmns; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RequestPathParameters { + + private String uriLdnFirstPart; + private String className; + private String id; + + /** + * Gets alternate id from combining URI-LDN-First-Part, className and id. + * + * @return String of alternate id. + */ + public String toAlternateId() { + return uriLdnFirstPart + "/" + className + "=" + id; + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/model/Resource.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/model/Resource.java index 9041e54ca2..716ff0fe19 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/model/Resource.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/model/Resource.java @@ -22,7 +22,6 @@ package org.onap.cps.ncmp.impl.provmns.model; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.onap.cps.ncmp.impl.provmns.model.ResourceOneOf; /** * This interface serves as a replacement for the generated Resource class, which has dependencies on the NRM-related diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorConfigurationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorConfigurationSpec.groovy index 960e6b32f3..ec846e1275 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorConfigurationSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorConfigurationSpec.groovy @@ -24,13 +24,14 @@ import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.ncmp.config.PolicyExecutorHttpClientConfig import org.onap.cps.ncmp.impl.policyexecutor.PolicyExecutorWebClientConfiguration import org.onap.cps.ncmp.utils.WebClientBuilderTestConfig +import org.onap.cps.utils.JsonObjectMapper import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.ContextConfiguration import spock.lang.Specification @SpringBootTest -@ContextConfiguration(classes = [ObjectMapper, PolicyExecutor, PolicyExecutorWebClientConfiguration, PolicyExecutorHttpClientConfig, WebClientBuilderTestConfig ]) +@ContextConfiguration(classes = [JsonObjectMapper, ObjectMapper, PolicyExecutor, PolicyExecutorWebClientConfiguration, PolicyExecutorHttpClientConfig, WebClientBuilderTestConfig ]) class PolicyExecutorConfigurationSpec extends Specification { @Autowired diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorSpec.groovy index 96330ab8ec..63260df9dc 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorSpec.groovy @@ -20,6 +20,7 @@ package org.onap.cps.ncmp.impl.data.policyexecutor +import com.fasterxml.jackson.core.JsonProcessingException; import ch.qos.logback.classic.Level import ch.qos.logback.classic.Logger import ch.qos.logback.classic.spi.ILoggingEvent @@ -29,6 +30,9 @@ import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.ncmp.api.exceptions.NcmpException import org.onap.cps.ncmp.api.exceptions.PolicyExecutorException import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle +import org.onap.cps.ncmp.impl.provmns.RequestPathParameters +import org.onap.cps.ncmp.impl.provmns.model.ResourceOneOf +import org.onap.cps.utils.JsonObjectMapper import org.slf4j.LoggerFactory import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity @@ -50,8 +54,9 @@ class PolicyExecutorSpec extends Specification { def mockRequestBodyUriSpec = Mock(WebClient.RequestBodyUriSpec) def mockResponseSpec = Mock(WebClient.ResponseSpec) def spiedObjectMapper = Spy(ObjectMapper) + def jsonObjectMapper = new JsonObjectMapper(spiedObjectMapper) - PolicyExecutor objectUnderTest = new PolicyExecutor(mockWebClient, spiedObjectMapper) + PolicyExecutor objectUnderTest = new PolicyExecutor(mockWebClient, spiedObjectMapper,jsonObjectMapper) def logAppender = Spy(ListAppender) @@ -223,6 +228,35 @@ class PolicyExecutorSpec extends Specification { thrownException.cause == webClientRequestException } + def 'Build policy executor operation details from ProvMnS request parameters where #scenario.'() { + given: 'a provMnsRequestParameter and a resource' + def path = new RequestPathParameters(uriLdnFirstPart: 'someUriLdnFirstPart', className: 'someClassName', id: 'someId') + def resource = new ResourceOneOf(id: 'someResourceId', attributes: ['someAttribute1:someValue1', 'someAttribute2:someValue2'], objectClass: objectClass) + when: 'a configurationManagementOperation is created and converted to JSON' + def result = objectUnderTest.buildOperationDetails(CREATE, path, resource) + then: 'the result is as expected (using json to compare)' + String expectedJsonString = '{"operation":"CREATE","targetIdentifier":"someUriLdnFirstPart/someClassName=someId","changeRequest":{"' + changeRequestClassReference + '":[{"id":"someId","attributes":["someAttribute1:someValue1","someAttribute2:someValue2"]}]}}' + assert jsonObjectMapper.asJsonString(result) == expectedJsonString + where: + scenario | objectClass || changeRequestClassReference + 'objectClass is populated' | 'someObjectClass' || 'someObjectClass' + 'objectClass is empty' | '' || 'someClassName' + 'objectClass is null' | null || 'someClassName' + } + + def 'Build Policy Executor Operation Details with a exception during conversion'() { + given: 'a provMnsRequestParameter and a resource' + def path = new RequestPathParameters(uriLdnFirstPart: 'someUriLdnFirstPart', className: 'someClassName', id: 'someId') + def resource = new ResourceOneOf(id: 'someResourceId', attributes: ['someAttribute1:someValue1', 'someAttribute2:someValue2']) + and: 'json object mapper throws an exception' + def originalException = new JsonProcessingException('some-exception') + spiedObjectMapper.readValue(*_) >> {throw originalException} + when: 'a configurationManagementOperation is created and converted to JSON' + objectUnderTest.buildOperationDetails(CREATE, path, resource) + then: 'the expected exception is throw and matches the original' + noExceptionThrown() + } + def mockResponse(mockResponseAsMap, httpStatus) { JsonNode jsonNode = spiedObjectMapper.readTree(spiedObjectMapper.writeValueAsString(mockResponseAsMap)) def mono = Mono.just(new ResponseEntity<>(jsonNode, httpStatus)) diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleQueryParametersValidatorSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleQueryParametersParameterMapperSpec.groovy similarity index 98% rename from cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleQueryParametersValidatorSpec.groovy rename to cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleQueryParametersParameterMapperSpec.groovy index 5a70928ede..e2ce433185 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleQueryParametersValidatorSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/CmHandleQueryParametersParameterMapperSpec.groovy @@ -25,7 +25,7 @@ import org.onap.cps.api.exceptions.DataValidationException import org.onap.cps.api.model.ConditionProperties import spock.lang.Specification -class CmHandleQueryParametersValidatorSpec extends Specification { +class CmHandleQueryParametersParameterMapperSpec extends Specification { def 'CM Handle Query validation: empty query.'() { given: 'a cm handle query' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParametersBuilderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParametersBuilderSpec.groovy new file mode 100644 index 0000000000..115fe569d9 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParametersBuilderSpec.groovy @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved. + * ================================================================================ + * 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.impl.provmns + +import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle +import org.onap.cps.ncmp.impl.provmns.model.ClassNameIdGetDataNodeSelectorParameter +import org.onap.cps.ncmp.impl.provmns.model.Scope +import spock.lang.Specification + +class ParametersBuilderSpec extends Specification{ + + def objectUnderTest = new ParametersBuilder() + + def 'Extract url template parameters for GET'() { + when:'a set of given parameters from a call are passed in' + def result = objectUnderTest.createUrlTemplateParametersForGet(new Scope(scopeLevel: 1, scopeType: 'BASE_ALL'), + 'my-filter', ['some-attribute'], ['some-field'], new ClassNameIdGetDataNodeSelectorParameter(dataNodeSelector: 'some-dataSelector'), + new YangModelCmHandle(dmiServiceName: 'some-dmi-service')) + then:'verify object has been mapped correctly' + result.urlVariables().get('filter') == 'my-filter' + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/utils/events/TopicValidatorSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/utils/events/TopicParameterMapperSpec.groovy similarity index 97% rename from cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/utils/events/TopicValidatorSpec.groovy rename to cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/utils/events/TopicParameterMapperSpec.groovy index 8fc3e18c7f..6d80da901f 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/utils/events/TopicValidatorSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/utils/events/TopicParameterMapperSpec.groovy @@ -23,7 +23,7 @@ package org.onap.cps.ncmp.utils.events import org.onap.cps.ncmp.api.exceptions.InvalidTopicException import spock.lang.Specification -class TopicValidatorSpec extends Specification { +class TopicParameterMapperSpec extends Specification { def 'Valid topic name validation.'() { when: 'a valid topic name is validated' diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/base/DmiDispatcher.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/base/DmiDispatcher.groovy index 828c374a9b..dd2a412343 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/base/DmiDispatcher.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/base/DmiDispatcher.groovy @@ -106,7 +106,7 @@ class DmiDispatcher extends Dispatcher { return mockWriteJobResponse(request) // provmns endpoint - case ~'^/ProvMnS/v1(.*)$': + case ~'^/ProvMnS/v1/(.*)$': dmiResourceDataUrl = request.path return mockResponseWithBody(HttpStatus.OK, '{}') diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/provmns/ProvMnSRestApiSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/provmns/ProvMnSRestApiSpec.groovy index 4fc6d19e9d..a0350eaefb 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/provmns/ProvMnSRestApiSpec.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/provmns/ProvMnSRestApiSpec.groovy @@ -36,7 +36,7 @@ class ProvMnSRestApiSpec extends CpsIntegrationSpecBase{ def 'Get Resource Data from provmns interface.'() { given: 'a registered cm handle' dmiDispatcher1.moduleNamesPerCmHandleId['ch-1'] = ['M1', 'M2'] - registerCmHandle(DMI1_URL, 'ch-1', NO_MODULE_SET_TAG, 'A=1/B=2/C=3') + registerCmHandle(DMI1_URL, 'ch-1', NO_MODULE_SET_TAG, '/A=1/B=2/C=3') expect: 'not implemented response on GET endpoint' mvc.perform(get("/ProvMnS/v1/A=1/B=2/C=3")) .andExpect(status().is2xxSuccessful()) @@ -46,22 +46,30 @@ class ProvMnSRestApiSpec extends CpsIntegrationSpecBase{ def 'Put Resource Data from provmns interface.'() { given: 'an example resource json body' + dmiDispatcher1.moduleNamesPerCmHandleId['ch-1'] = ['M1', 'M2'] + registerCmHandle(DMI1_URL, 'ch-1', NO_MODULE_SET_TAG, '/A=1/B=2/C=3') def jsonBody = jsonObjectMapper.asJsonString(new ResourceOneOf('test')) expect: 'not implemented response on PUT endpoint' mvc.perform(put("/ProvMnS/v1/A=1/B=2/C=3") .contentType(MediaType.APPLICATION_JSON) .content(jsonBody)) - .andExpect(status().isNotImplemented()) + .andExpect(status().isOk()) + cleanup: 'deregister CM handles' + deregisterCmHandle(DMI1_URL, 'ch-1') } def 'Patch Resource Data from provmns interface.'() { given: 'an example resource json body' + dmiDispatcher1.moduleNamesPerCmHandleId['ch-1'] = ['M1', 'M2'] + registerCmHandle(DMI1_URL, 'ch-1', NO_MODULE_SET_TAG, '/A=1/B=2/C=3') def jsonBody = jsonObjectMapper.asJsonString(new ResourceOneOf('test')) expect: 'not implemented response on PATCH endpoint' mvc.perform(patch("/ProvMnS/v1/A=1/B=2/C=3") .contentType(new MediaType('application', 'json-patch+json')) .content(jsonBody)) .andExpect(status().isNotImplemented()) + cleanup: 'deregister CM handles' + deregisterCmHandle(DMI1_URL, 'ch-1') } def 'Delete Resource Data from provmns interface.'() { -- 2.16.6