From: seanbeirne Date: Tue, 11 Nov 2025 15:50:01 +0000 (+0000) Subject: Implement PATCH for ProvMnS interface with bug fixes X-Git-Url: https://gerrit.onap.org/r/gitweb?a=commitdiff_plain;h=6d17c07706cc3cd5a34709a68c4068596b48c7e2;p=cps.git Implement PATCH for ProvMnS interface with bug fixes -Implement PATCH endpoint with error handling -Missing # attribute update operation; will come in later patch -Fixes error return bug where all errors defaulted to 500 -Fixes get call to dmi in put operation -Moved Resource to body in fowarding Issue-ID: CPS-2704 Change-Id: I06bf643b155410a24774c5af9d6016c2d70a2219 Signed-off-by: seanbeirne --- 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 68b6151fc3..d9840d5514 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 @@ -34,6 +34,7 @@ import org.onap.cps.ncmp.impl.provmns.model.ClassNameIdGetDataNodeSelectorParame import org.onap.cps.ncmp.impl.provmns.model.ClassNameIdPatchDefaultResponse; 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.PatchItem; import org.onap.cps.ncmp.impl.provmns.model.Resource; import org.onap.cps.ncmp.impl.provmns.model.Scope; import org.onap.cps.ncmp.rest.model.ErrorMessage; @@ -160,7 +161,7 @@ public interface ProvMnS { * and the patch document included in the request message body. * * @param httpServletRequest (required) - * @param resource The request body describes changes to be made to the target resources. + * @param patchItems The request body describes changes to be made to the target resources. * The following patch media types are available * - "application/json-patch+json" (RFC 6902) * - "application/3gpp-json-patch+json" (TS 32.158) (required) @@ -212,7 +213,7 @@ public interface ProvMnS { + "resources. The following patch media types are available " + "- \"application/json-patch+json\" (RFC 6902) " + "- \"application/3gpp-json-patch+json\" (TS 32.158)", required = true) @Valid @RequestBody - Resource resource + List patchItems ); 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 3286dc566a..79acf2dc2e 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 @@ -36,6 +36,7 @@ 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.PatchItem; 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; @@ -83,12 +84,12 @@ public class ProvMnsController implements ProvMnS { ? HttpStatus.NOT_ACCEPTABLE : HttpStatus.UNPROCESSABLE_ENTITY; return errorResponseBuilder.buildErrorResponseGet(httpStatus, exception.getDetails()); } - final UrlTemplateParameters urlTemplateParameters = parametersBuilder.createUrlTemplateParametersForGet( + final UrlTemplateParameters urlTemplateParameters = parametersBuilder.createUrlTemplateParametersForRead( scope, filter, attributes, fields, dataNodeSelector, - yangModelCmHandle); + yangModelCmHandle, requestPathParameters); return dmiRestClient.synchronousGetOperation( - RequiredDmiService.DATA, urlTemplateParameters, OperationType.READ); + RequiredDmiService.DATA, urlTemplateParameters); } catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) { final String reason = buildNotFoundMessage(requestPathParameters.toAlternateId()); return errorResponseBuilder.buildErrorResponseGet(HttpStatus.NOT_FOUND, reason); @@ -96,18 +97,41 @@ public class ProvMnsController implements ProvMnS { } @Override - public ResponseEntity patchMoi(final HttpServletRequest httpServletRequest, final Resource resource) { + public ResponseEntity patchMoi(final HttpServletRequest httpServletRequest, + final List patchItems) { 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.buildErrorResponsePatch(httpStatus, exception.getDetails()); + } + try { + policyExecutor.checkPermission(yangModelCmHandle, + OperationType.CREATE, + NO_AUTHORIZATION, + requestPathParameters.toAlternateId(), + jsonObjectMapper.asJsonString(policyExecutor.buildPatchOperationDetails( + requestPathParameters, patchItems)) + ); + } catch (final RuntimeException exception) { + // Policy Executor Denied Execution + return errorResponseBuilder.buildErrorResponsePatch(HttpStatus.NOT_ACCEPTABLE, exception.getMessage()); + } + final UrlTemplateParameters urlTemplateParameters = + parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestPathParameters); + return dmiRestClient.synchronousPatchOperation(RequiredDmiService.DATA, patchItems, + urlTemplateParameters, httpServletRequest.getContentType()); } catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) { final String reason = buildNotFoundMessage(requestPathParameters.toAlternateId()); return errorResponseBuilder.buildErrorResponsePatch(HttpStatus.NOT_FOUND, reason); } - return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } @Override @@ -128,9 +152,9 @@ public class ProvMnsController implements ProvMnS { try { policyExecutor.checkPermission(yangModelCmHandle, OperationType.CREATE, - null, + NO_AUTHORIZATION, requestPathParameters.toAlternateId(), - jsonObjectMapper.asJsonString(policyExecutor.buildOperationDetails( + jsonObjectMapper.asJsonString(policyExecutor.buildCreateOperationDetails( OperationType.CREATE, requestPathParameters, resource)) ); } catch (final RuntimeException exception) { @@ -138,10 +162,8 @@ public class ProvMnsController implements ProvMnS { exception.getMessage()); } final UrlTemplateParameters urlTemplateParameters = - parametersBuilder.createUrlTemplateParametersForPut(resource, - yangModelCmHandle); - return dmiRestClient.synchronousPutOperation( - RequiredDmiService.DATA, urlTemplateParameters, OperationType.CREATE); + parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestPathParameters); + return dmiRestClient.synchronousPutOperation(RequiredDmiService.DATA, resource, urlTemplateParameters); } catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) { final String reason = buildNotFoundMessage(requestPathParameters.toAlternateId()); return errorResponseBuilder.buildErrorResponseDefault(HttpStatus.NOT_FOUND, reason); @@ -176,7 +198,7 @@ public class ProvMnsController implements ProvMnS { exception.getMessage()); } final UrlTemplateParameters urlTemplateParameters = - parametersBuilder.createUrlTemplateParametersForDelete(yangModelCmHandle); + parametersBuilder.createUrlTemplateParametersForWrite(yangModelCmHandle, requestPathParameters); return dmiRestClient.synchronousDeleteOperation(RequiredDmiService.DATA, urlTemplateParameters); } catch (final NoAlternateIdMatchFoundException noAlternateIdMatchFoundException) { final String reason = buildNotFoundMessage(requestPathParameters.toAlternateId()); 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 index 8be88d50ea..50b99c25d3 100644 --- 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 @@ -42,7 +42,7 @@ public class ErrorResponseBuilder { * Default is used by PUT and DELETE * * @param httpStatus HTTP response - * @param reason reason for error response + * @param reason reason for error response * @return response entity */ public ResponseEntity buildErrorResponseDefault(final HttpStatus httpStatus, final String reason) { @@ -56,7 +56,7 @@ public class ErrorResponseBuilder { * Create response entity for get error response. * * @param httpStatus HTTP response - * @param reason reason for error response + * @param reason reason for error response * @return response entity */ public ResponseEntity buildErrorResponseGet(final HttpStatus httpStatus, final String reason) { 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 a858f3d98a..cecf24c129 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,12 +22,16 @@ 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.exceptions.ProvMnSException 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.provmns.model.PatchItem +import org.onap.cps.ncmp.impl.provmns.model.PatchOperation import org.onap.cps.ncmp.impl.provmns.model.ResourceOneOf import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher import org.onap.cps.ncmp.rest.provmns.ErrorResponseBuilder @@ -82,6 +86,7 @@ class ProvMnsControllerSpec extends Specification { JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) def jsonBody = jsonObjectMapper.asJsonString(new ResourceOneOf('test')) + def patchJsonBody = jsonObjectMapper.asJsonString([new PatchItem(op: 'REMOVE', path: 'someUriLdnFirstPart')]) @Value('${rest.api.provmns-base-path}') def provMnSBasePath @@ -102,7 +107,7 @@ class ProvMnsControllerSpec extends Specification { where: scenario | dataProducerId | state || expectedHttpStatus 'cmHandle state is Ready with populated dataProducerId' | 'someDataProducerId' | READY || HttpStatus.OK - 'dataProducerId is empty' | '' | READY || HttpStatus.UNPROCESSABLE_ENTITY + 'dataProducerId is blank' | ' ' | READY || HttpStatus.UNPROCESSABLE_ENTITY 'dataProducerId is null' | null | READY || HttpStatus.UNPROCESSABLE_ENTITY 'cmHandle state is Advised' | 'someDataProducerId' | ADVISED || HttpStatus.NOT_ACCEPTABLE } @@ -120,34 +125,64 @@ class ProvMnsControllerSpec extends Specification { 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" - when: 'patch data resource request is performed' - def response = mvc.perform(patch(patchUrl) + def 'Patch request where #scenario'() { + given: 'provmns url' + def provmnsUrl = "$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.synchronousPatchOperation(*_) >> new ResponseEntity<>('Some response from DMI service', HttpStatus.OK) + when: 'patch request is performed' + def response = mvc.perform(patch(provmnsUrl) .contentType(new MediaType('application', 'json-patch+json')) - .content(jsonBody)) + .content(patchJsonBody)) .andReturn().response - then: 'response status is Not Implemented (501)' - assert response.status == HttpStatus.NOT_IMPLEMENTED.value() + 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 blank' | ' ' | READY || HttpStatus.UNPROCESSABLE_ENTITY + 'dataProducerId is null' | null | READY || HttpStatus.UNPROCESSABLE_ENTITY + 'cmHandle state is Advised' | 'someDataProducerId' | ADVISED || HttpStatus.NOT_ACCEPTABLE } - def 'Patch resource data request with no match for alternate id'() { + def 'Patch request with no match for alternate id'() { given: 'resource data url' - def patchUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId" + def provmnsUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId" and: 'alternate Id cannot be matched' - alternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(*_) >> {throw new NoAlternateIdMatchFoundException('/someUriLdnFirstPart/someClassName=someId')} + 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(patchUrl) - .contentType("application/json-patch+json") - .content(jsonBody)) + inventoryPersistence.getYangModelCmHandle('cm-1') >> new YangModelCmHandle(id:'cm-1', dmiServiceName: 'someDmiService', dataProducerIdentifier: 'someDataProducerId', compositeState: new CompositeState(cmHandleState: READY)) + when: 'patch request is performed' + def response = mvc.perform(patch(provmnsUrl) + .contentType(new MediaType('application', 'json-patch+json')) + .content(patchJsonBody)) .andReturn().response then: 'response status is NOT_FOUND (404)' assert response.status == HttpStatus.NOT_FOUND.value() } + def 'Patch request with no permission from coordination management'() { + given: 'resource data url' + def url = "$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(patch(url) + .contentType(new MediaType('application', 'json-patch+json')) + .content(patchJsonBody)) + .andReturn().response + then: 'response status is NOT_ACCEPTABLE (406)' + assert response.status == HttpStatus.NOT_ACCEPTABLE.value() + } + def 'Put resource data request where #scenario'() { given: 'resource data url' def putUrl = "$provMnSBasePath/v1/someUriLdnFirstPart/someClassName=someId" @@ -167,7 +202,7 @@ class ProvMnsControllerSpec extends Specification { where: scenario | dataProducerId | state || expectedHttpStatus 'valid request is made' | 'someDataProducerId' | READY || HttpStatus.OK - 'dataProducerId is empty' | '' | READY || HttpStatus.UNPROCESSABLE_ENTITY + 'dataProducerId is blank' | ' ' | READY || HttpStatus.UNPROCESSABLE_ENTITY 'dataProducerId is null' | null | READY || HttpStatus.UNPROCESSABLE_ENTITY 'cmHandle state is Advised' | 'someDataProducerId' | ADVISED || HttpStatus.NOT_ACCEPTABLE } @@ -220,11 +255,11 @@ class ProvMnsControllerSpec extends Specification { 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 + scenario | dataProducerId | state || expectedHttpStatus + 'valid request is made' | 'someDataProducerId' | READY || HttpStatus.OK + 'dataProducerId is blank' | ' ' | 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'() { 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/CreateOperationDetails.java similarity index 84% rename from cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetails.java rename to cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/CreateOperationDetails.java index 16a2918907..60a944db4c 100644 --- 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/CreateOperationDetails.java @@ -25,6 +25,6 @@ import java.util.List; import java.util.Map; @JsonInclude(JsonInclude.Include.NON_NULL) -public record OperationDetails(String operation, - String targetIdentifier, - Map> changeRequest) {} +public record CreateOperationDetails(String operation, + String targetIdentifier, + Map> changeRequest) {} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/PatchOperationsDetails.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/PatchOperationsDetails.java new file mode 100644 index 0000000000..d456507571 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/PatchOperationsDetails.java @@ -0,0 +1,25 @@ +/* + * ============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 java.util.List; + +public record PatchOperationsDetails (String permissionId, String changeRequestFormat, List operations) {} 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 8dd9881b09..d4dbe3fc42 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 @@ -27,6 +27,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.net.UnknownHostException; import java.time.Duration; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -40,6 +41,7 @@ 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.PatchItem; 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; @@ -123,16 +125,42 @@ public class PolicyExecutor { } /** - * Build a OperationDetails object from ProvMnS request details. + * Build a PatchOperationDetails 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 patchItems provided request list of patch Items + * @return CreateOperationDetails object + */ + public PatchOperationsDetails buildPatchOperationDetails(final RequestPathParameters requestPathParameters, + final List patchItems) { + final List operations = new ArrayList<>(patchItems.size()); + for (final PatchItem patchItem : patchItems) { + switch (patchItem.getOp()) { + case ADD -> operations.add( + buildCreateOperationDetails(OperationType.CREATE, requestPathParameters, + (Resource) patchItem.getValue())); + case REPLACE -> operations.add( + buildCreateOperationDetails(OperationType.UPDATE, requestPathParameters, + (Resource) patchItem.getValue())); + case REMOVE -> operations.add( + buildDeleteOperationDetails(requestPathParameters.toAlternateId())); + default -> log.warn("Unsupported Patch Operation Type:{}", patchItem.getOp().getValue()); + }; + } + return new PatchOperationsDetails("Some Permission Id", CHANGE_REQUEST_FORMAT, operations); + } + + /** + * Build a CreateOperationDetails object from ProvMnS request details. + * + * @param operationType Type of operation create, update. * @param requestPathParameters request parameters including uri-ldn-first-part, className and id * @param resource provided request resource - * @return OperationDetails object + * @return CreateOperationDetails object */ - public OperationDetails buildOperationDetails(final OperationType operationType, - final RequestPathParameters requestPathParameters, - final Resource resource) { + public CreateOperationDetails buildCreateOperationDetails(final OperationType operationType, + final RequestPathParameters requestPathParameters, + final Resource resource) { final Map> changeRequest = new HashMap<>(); final OperationEntry operationEntry = new OperationEntry(); @@ -151,7 +179,8 @@ public class PolicyExecutor { log.debug("JSON processing error: {}", exception); } changeRequest.put(className, List.of(operationEntry)); - return new OperationDetails(operationType.name(), requestPathParameters.toAlternateId(), changeRequest); + return new CreateOperationDetails(operationType.name(), + requestPathParameters.getUriLdnFirstPart(), changeRequest); } /** 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 9939e610a0..26f46638e3 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 @@ -42,6 +42,7 @@ import org.onap.cps.utils.JsonObjectMapper; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.BodyInserters; @@ -76,7 +77,7 @@ public class DmiRestClient { * @param requestBodyAsJsonString JSON data body. * @param operationType The type of operation being executed (for error reporting only). * @param authorization Contents of the Authorization header, or null if not present. - * @return ResponseEntity containing the response from the DMI. + * @return ResponseEntity containing the response from the DMI. * @throws DmiClientRequestException If there is an error during the DMI request. */ public ResponseEntity synchronousPostOperation(final RequiredDmiService requiredDmiService, @@ -100,7 +101,7 @@ public class DmiRestClient { * @param operationType An enumeration or object that holds information about the type of operation * being performed. * @param authorization The authorization token to be added to the request headers. - * @return A Mono emitting the response entity containing the server's response. + * @return A Mono emitting the response entity containing the server's response. */ public Mono> asynchronousPostOperation(final RequiredDmiService requiredDmiService, final UrlTemplateParameters urlTemplateParameters, @@ -118,47 +119,65 @@ public class DmiRestClient { } /** - * Sends a synchronous (blocking) GET operation to the DMI. + * Sends a synchronous (blocking) GET operation to the DMI without error mapping. * * @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. + * @return ResponseEntity containing the response from the DMI. */ public ResponseEntity synchronousGetOperation(final RequiredDmiService requiredDmiService, - final UrlTemplateParameters urlTemplateParameters, - final OperationType operationType) { + final UrlTemplateParameters urlTemplateParameters) { 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(); } /** - * Sends a synchronous (blocking) PUT operation to the DMI. + * Sends a synchronous (blocking) PUT operation to the DMI without error mapping. * * @param requiredDmiService Determines if the required service is for a data or model operation. + * @param body resource object to be forwarded. * @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. + * @return ResponseEntity containing the response from the DMI. */ public ResponseEntity synchronousPutOperation(final RequiredDmiService requiredDmiService, + final Object body, + final UrlTemplateParameters urlTemplateParameters) { + return getWebClient(requiredDmiService) + .put() + .uri(urlTemplateParameters.urlTemplate(), urlTemplateParameters.urlVariables()) + .headers(httpHeaders -> configureHttpHeaders(httpHeaders, NO_AUTHORIZATION)) + .bodyValue(body) + .retrieve() + .toEntity(Object.class) + .block(); + } + + /** + * Sends a synchronous (blocking) PATCH operation to the DMI without error mapping. + * + * @param requiredDmiService Determines if the required service is for a data or model operation. + * @param body object + * @param urlTemplateParameters The DMI resource URL template with variables. + * @param contentType Content type example: application/json + * @return ResponseEntity containing the response from the DMI. + */ + public ResponseEntity synchronousPatchOperation(final RequiredDmiService requiredDmiService, + final Object body, final UrlTemplateParameters urlTemplateParameters, - final OperationType operationType) { + final String contentType) { return getWebClient(requiredDmiService) - .get() + .patch() .uri(urlTemplateParameters.urlTemplate(), urlTemplateParameters.urlVariables()) .headers(httpHeaders -> configureHttpHeaders(httpHeaders, NO_AUTHORIZATION)) + .contentType(MediaType.parseMediaType(contentType)) + .bodyValue(body) .retrieve() .toEntity(Object.class) - .onErrorMap(throwable -> handleDmiClientException(throwable, operationType.getOperationName())) .block(); } @@ -193,7 +212,7 @@ public class DmiRestClient { * * @param urlTemplateParameters The URL template parameters for the DMI data endpoint. * @param authorization The authorization token to be added to the request headers. - * @return A Mono emitting the result of the request as a String. + * @return A Mono emitting the result of the request as a String. */ public Mono asynchronousDmiDataRequest(final UrlTemplateParameters urlTemplateParameters, final String authorization) { @@ -207,7 +226,7 @@ public class DmiRestClient { } /** - * Sends a synchronous (blocking) DELETE operation to the DMI with a JSON body. + * Sends a synchronous (blocking) DELETE operation to the DMI with a JSON body without error mapping. * * @param requiredDmiService Determines if the required service is for a data or model operation. * @param urlTemplateParameters The DMI resource URL template with variables. @@ -223,7 +242,6 @@ public class DmiRestClient { .headers(httpHeaders -> configureHttpHeaders(httpHeaders, NO_AUTHORIZATION)) .retrieve() .toEntity(Object.class) - .onErrorMap(throwable -> handleDmiClientException(throwable, OperationType.DELETE.getOperationName())) .block(); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParametersBuilder.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParametersBuilder.java index cf85d740e3..2e34a3bab1 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParametersBuilder.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParametersBuilder.java @@ -20,11 +20,12 @@ package org.onap.cps.ncmp.impl.provmns; +import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATA; + import java.util.List; import lombok.RequiredArgsConstructor; 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; @@ -35,7 +36,7 @@ import org.springframework.stereotype.Service; public class ParametersBuilder { /** - * Creates a UrlTemplateParameters object containing the relevant fields for a get. + * Creates a UrlTemplateParameters object containing the relevant fields for read requests. * * @param scope Provided className parameter. * @param filter Filter string. @@ -45,50 +46,47 @@ public class ParametersBuilder { * @param yangModelCmHandle yangModelCmHandle object for resolved alternate ID * @return UrlTemplateParameters object. */ - public UrlTemplateParameters createUrlTemplateParametersForGet(final Scope scope, final String filter, - final List attributes, - final List fields, - final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector, - final YangModelCmHandle yangModelCmHandle) { + public UrlTemplateParameters createUrlTemplateParametersForRead(final Scope scope, + final String filter, + final List attributes, + final List fields, + final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector, + final YangModelCmHandle yangModelCmHandle, + final RequestPathParameters requestPathParameters) { + final String dmiServiceName = yangModelCmHandle.resolveDmiServiceName(DATA); + final String attributesString = removeBrackets(attributes); + final String fieldsString = removeBrackets(fields); return RestServiceUrlTemplateBuilder.newInstance() - .fixedPathSegment(yangModelCmHandle.getAlternateId()) - .queryParameter("scopeType", scope.getScopeType() != null - ? scope.getScopeType().getValue() : null) - .queryParameter("scopeLevel", scope.getScopeLevel() != null - ? scope.getScopeLevel().toString() : null) + .fixedPathSegment(requestPathParameters.toAlternateId()) + .queryParameter("scopeType", scope.getScopeType() != null ? scope.getScopeType().getValue() : null) + .queryParameter("scopeLevel", scope.getScopeLevel() != null ? scope.getScopeLevel().toString() : null) .queryParameter("filter", filter) - .queryParameter("attributes", attributes != null ? attributes.toString() : null) - .queryParameter("fields", fields != null ? fields.toString() : null) + .queryParameter("attributes", attributesString.isBlank() ? null : attributesString) + .queryParameter("fields", fieldsString.isBlank() ? null : fieldsString) .queryParameter("dataNodeSelector", dataNodeSelector.getDataNodeSelector()) - .createUrlTemplateParameters(yangModelCmHandle.getDmiServiceName(), "ProvMnS"); + .createUrlTemplateParameters(dmiServiceName, "ProvMnS"); } /** - * Creates a UrlTemplateParameters object containing the relevant fields for a put. + * Creates a UrlTemplateParameters object containing the relevant fields for a write requests. * - * @param resource Provided resource parameter. - * @param yangModelCmHandle yangModelCmHandle object for resolved alternate ID + * @param yangModelCmHandle yangModelCmHandle object for resolved alternate ID + * @param requestPathParameters request path parameters. * @return UrlTemplateParameters object. */ - public UrlTemplateParameters createUrlTemplateParametersForPut(final Resource resource, - final YangModelCmHandle yangModelCmHandle) { - + public UrlTemplateParameters createUrlTemplateParametersForWrite(final YangModelCmHandle yangModelCmHandle, + final RequestPathParameters requestPathParameters) { + final String dmiServiceName = yangModelCmHandle.resolveDmiServiceName(DATA); return RestServiceUrlTemplateBuilder.newInstance() - .fixedPathSegment(yangModelCmHandle.getAlternateId()) - .queryParameter("resource", resource.toString()) - .createUrlTemplateParameters(yangModelCmHandle.getDmiServiceName(), "ProvMnS"); + .fixedPathSegment(requestPathParameters.toAlternateId()) + .createUrlTemplateParameters(dmiServiceName, "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"); + private String removeBrackets(final List queryParameterList) { + if (queryParameterList != null) { + final String queryParameterText = queryParameterList.toString(); + return queryParameterText.substring(1, queryParameterText.length() - 1); + } + return ""; } -} \ No newline at end of file +} 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 63260df9dc..4ad7cbafad 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 @@ -29,8 +29,10 @@ import com.fasterxml.jackson.databind.JsonNode 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.api.exceptions.ProvMnSException 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.PatchItem import org.onap.cps.ncmp.impl.provmns.model.ResourceOneOf import org.onap.cps.utils.JsonObjectMapper import org.slf4j.LoggerFactory @@ -228,14 +230,42 @@ class PolicyExecutorSpec extends Specification { thrownException.cause == webClientRequestException } - def 'Build policy executor operation details from ProvMnS request parameters where #scenario.'() { + def 'Build policy executor patch operation details from ProvMnS request parameters where #scenario.'() { + given: 'a provMnsRequestParameter and a patchItem list' + def path = new RequestPathParameters(uriLdnFirstPart: 'someUriLdnFirstPart', className: 'someClassName', id: 'someId') + def resource = new ResourceOneOf(id: 'someResourceId', attributes: ['someAttribute1:someValue1', 'someAttribute2:someValue2'], objectClass: objectClass) + def patchItemsList = [new PatchItem(op: 'ADD', 'path':'someUriLdnFirstPart', value: resource), new PatchItem(op: 'REPLACE', 'path':'someUriLdnFirstPart', value: resource)] + when: 'a configurationManagementOperation is created and converted to JSON' + def result = objectUnderTest.buildPatchOperationDetails(path, patchItemsList) + then: 'the result is as expected (using json to compare)' + def expectedJsonString = '{"permissionId":"Some Permission Id","changeRequestFormat":"cm-legacy","operations":[{"operation":"CREATE","targetIdentifier":"someUriLdnFirstPart","changeRequest":{"' + changeRequestClassReference + '":[{"id":"someId","attributes":["someAttribute1:someValue1","someAttribute2:someValue2"]}]}},{"operation":"UPDATE","targetIdentifier":"someUriLdnFirstPart","changeRequest":{"' + changeRequestClassReference + '":[{"id":"someId","attributes":["someAttribute1:someValue1","someAttribute2:someValue2"]}]}}]}' + assert expectedJsonString == jsonObjectMapper.asJsonString(result) + where: + scenario | objectClass || changeRequestClassReference + 'objectClass is populated' | 'someObjectClass' || 'someObjectClass' + 'objectClass is empty' | '' || 'someClassName' + 'objectClass is null' | null || 'someClassName' + } + + def 'Build policy executor patch operation details from ProvMnS request parameters with invalid op.'() { + given: 'a provMnsRequestParameter and a patchItem list' + def path = new RequestPathParameters(uriLdnFirstPart: 'someUriLdnFirstPart', className: 'someClassName', id: 'someId') + def patchItemsList = [new PatchItem(op: 'TEST', 'path':'someUriLdnFirstPart')] + when: 'a configurationManagementOperation is created and converted to JSON' + def result = objectUnderTest.buildPatchOperationDetails(path, patchItemsList) + then: 'the result is as expected (using json to compare)' + def expectedJsonString = '{"permissionId":"Some Permission Id","changeRequestFormat":"cm-legacy","operations":[]}' + assert expectedJsonString == jsonObjectMapper.asJsonString(result) + } + + def 'Build policy executor create 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) + def result = objectUnderTest.buildCreateOperationDetails(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"]}]}}' + String expectedJsonString = '{"operation":"CREATE","targetIdentifier":"someUriLdnFirstPart","changeRequest":{"' + changeRequestClassReference + '":[{"id":"someId","attributes":["someAttribute1:someValue1","someAttribute2:someValue2"]}]}}' assert jsonObjectMapper.asJsonString(result) == expectedJsonString where: scenario | objectClass || changeRequestClassReference @@ -252,7 +282,7 @@ class PolicyExecutorSpec extends Specification { def originalException = new JsonProcessingException('some-exception') spiedObjectMapper.readValue(*_) >> {throw originalException} when: 'a configurationManagementOperation is created and converted to JSON' - objectUnderTest.buildOperationDetails(CREATE, path, resource) + objectUnderTest.buildCreateOperationDetails(CREATE, path, resource) then: 'the expected exception is throw and matches the original' noExceptionThrown() } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/dmi/DmiRestClientIntegrationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/dmi/DmiRestClientIntegrationSpec.groovy index 8d70591433..f5b9835e7d 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/dmi/DmiRestClientIntegrationSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/dmi/DmiRestClientIntegrationSpec.groovy @@ -33,7 +33,6 @@ import spock.lang.Specification import static org.onap.cps.ncmp.api.data.models.OperationType.CREATE import static org.onap.cps.ncmp.api.data.models.OperationType.READ -import static org.onap.cps.ncmp.api.data.models.OperationType.UPDATE import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATA import static org.onap.cps.ncmp.impl.models.RequiredDmiService.MODEL @@ -60,13 +59,16 @@ class DmiRestClientIntegrationSpec extends Specification { def result switch(method) { case 'get': - result = objectUnderTest.synchronousGetOperation(DATA, urlTemplateParameters, READ) + result = objectUnderTest.synchronousGetOperation(DATA, urlTemplateParameters) break case 'post': result = objectUnderTest.synchronousPostOperation(DATA, urlTemplateParameters, 'body', CREATE, '') break case 'put': - result = objectUnderTest.synchronousPutOperation(DATA, urlTemplateParameters, UPDATE) + result = objectUnderTest.synchronousPutOperation(DATA, 'body', urlTemplateParameters) + break + case 'patch': + result = objectUnderTest.synchronousPatchOperation(DATA, 'body', urlTemplateParameters, 'application/json-patch+json') break case 'delete': result = objectUnderTest.synchronousDeleteOperation(DATA, urlTemplateParameters) @@ -74,41 +76,27 @@ class DmiRestClientIntegrationSpec extends Specification { then: 'the result has the same status code of 200' assert result.statusCode.value() == 200 where: 'the following http methods are used' - method << ['get', 'post', 'put', 'delete'] + method << ['get', 'post', 'put', 'patch', 'delete'] } - def 'Synchronous DMI #method request with invalid JSON.'() { + def 'Synchronous DMI post request with invalid JSON.'() { given: 'Web Server wil return OK response' mockWebServer.enqueue(new MockResponse().setResponseCode(HttpStatus.OK.value) .setBody('invalid-json:!!') .addHeader('Content-Type', 'application/json')) - when: 'synchronous #method request is attempted (on Model service this time for coverage on service selector)' - switch(method) { - case 'get': - objectUnderTest.synchronousGetOperation(MODEL, urlTemplateParameters, READ) - break; - case 'post': - objectUnderTest.synchronousPostOperation(MODEL, urlTemplateParameters, 'body', READ, 'some authorization') - break - case 'put': - objectUnderTest.synchronousPutOperation(MODEL, urlTemplateParameters, UPDATE) - break - case 'delete': - objectUnderTest.synchronousDeleteOperation(MODEL, urlTemplateParameters) - } + when: 'synchronous post request is attempted (on Model service this time for coverage on service selector)' + objectUnderTest.synchronousPostOperation(MODEL, urlTemplateParameters, 'body', READ, 'some authorization') then: 'a dmi client request exception is thrown with the correct error codes' def thrown = thrown(DmiClientRequestException) assert thrown.getHttpStatusCode() == 500 assert thrown.ncmpResponseStatus.code == '108' - where: 'the following http methods are used' - method << ['get','post','put','delete'] } def 'DMI Request with non-responding server.'() { given: 'the web server is shut down' mockWebServer.shutdown() - when: 'a synchronous read request is attempted' - objectUnderTest.synchronousGetOperation(DATA, urlTemplateParameters, READ) + when: 'a synchronous post request is attempted' + objectUnderTest.synchronousPostOperation(DATA, urlTemplateParameters,'body', CREATE, '' ) then: 'a dmi client request exception is thrown with status code of 503 Service Unavailable' def thrown = thrown(DmiClientRequestException) assert thrown.getHttpStatusCode() == 503 @@ -117,8 +105,8 @@ class DmiRestClientIntegrationSpec extends Specification { def 'DMI Request with #scenario.'() { given: 'the mock server or exception setup' mockWebServer.enqueue(new MockResponse().setResponseCode(responseCode.value)) - when: 'a synchronous read request is attempted' - objectUnderTest.synchronousGetOperation(DATA, urlTemplateParameters, READ) + when: 'a synchronous post request is attempted' + objectUnderTest.synchronousPostOperation(DATA, urlTemplateParameters,'body', CREATE, '') then: 'a DMI client request exception is thrown with the right status' def thrown = thrown(DmiClientRequestException) assert thrown.httpStatusCode == expectedStatus @@ -133,7 +121,7 @@ class DmiRestClientIntegrationSpec extends Specification { given: 'Mock a bad URL that causes IllegalArgumentException before HTTP call' def badUrlParameters = new UrlTemplateParameters(':://bad url', [someParam: 'value']) when: 'a synchronous request is attempted' - objectUnderTest.synchronousGetOperation(DATA, badUrlParameters, READ) + objectUnderTest.synchronousGetOperation(DATA, badUrlParameters) then: 'a invalid url exception is thrown (no mapping)' thrown(InvalidUrlException) } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParameterMapperSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParameterMapperSpec.groovy new file mode 100644 index 0000000000..9acd4bc802 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParameterMapperSpec.groovy @@ -0,0 +1,67 @@ +/* + * ============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 jakarta.servlet.http.HttpServletRequest +import org.onap.cps.ncmp.api.exceptions.ProvMnSException +import spock.lang.Specification + +class ParameterMapperSpec extends Specification { + + def objectUnderTest = new ParameterMapper() + + def mockHttpServletRequest = Mock(HttpServletRequest) + + def uriPathAttributeName = 'org.springframework.web.servlet.HandlerMapping.pathWithinHandlerMapping' + + def 'Extract request parameters with url first part is a FDN with #scenario.'() { + given: 'a http request with all the required parts' + mockHttpServletRequest.getAttribute(uriPathAttributeName) >> (String) "ProvMnS/v1/${fdnPrefix}/myClass=myId" + when: 'the request parameters are extracted' + def result = objectUnderTest.extractRequestParameters(mockHttpServletRequest) + then: 'the Uri LDN first part is the fdnPrefix preceded with an extra "/"' + assert result.uriLdnFirstPart == '/' + fdnPrefix + and: 'the class name and id are mapped correctly' + assert result.className == 'myClass' + assert result.id == 'myId' + where: 'The following FDN prefixes are used' + scenario | fdnPrefix + '1 segment' | 'somePrefix' + 'multiple segments' | 'some/prefix' + 'empty segment' | '' + } + + def 'Attempt to extract request parameters with #scenario.'() { + given: 'a http request with all the required parts' + mockHttpServletRequest.getAttribute(uriPathAttributeName) >> path + when: 'attempt to extract the request parameters' + objectUnderTest.extractRequestParameters(mockHttpServletRequest) + then: 'a ProvMnS exception is thrown with message about the path being invalid' + def thrown = thrown(ProvMnSException) + assert thrown.message == 'not a valid path' + then: 'the details contain the faulty path' + assert thrown.details.contains(path) + where: 'the following paths are used' + scenario | path + 'No / in URI first part' | 'ProvMnS/v1/myClass=myId' + 'No = After (last) class name' | 'ProvMnS/v1/someOtherClass=someId/myClass' + } + +} 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 index 115fe569d9..6470131856 100644 --- 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 @@ -28,12 +28,53 @@ 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' + def 'Create url template parameters for GET'() { + when: 'Creating URL parameters for GET with all possible items' + def result = objectUnderTest.createUrlTemplateParametersForRead( + new Scope(scopeLevel: 1, scopeType: 'BASE_SUBTREE'), + 'my filter', + ['my attributes'], + ['my fields'], + new ClassNameIdGetDataNodeSelectorParameter(dataNodeSelector: 'my dataNodeSelector'), + new YangModelCmHandle(dmiServiceName: 'myDmiService'), + new RequestPathParameters(uriLdnFirstPart:'myPathVariable=myPathValue', className: 'myClassName', id:'myId')) + then: 'the template has the correct correct' + assert result.urlTemplate.toString().startsWith('myDmiService/ProvMnS/v1/myPathVariable=myPathValue/myClassName=myId') + and: 'all url variable have been set correctly' + assert result.urlVariables.size() == 6 + assert result.urlVariables.scopeLevel == '1' + assert result.urlVariables.scopeType == 'BASE_SUBTREE' + assert result.urlVariables.filter == 'my filter' + assert result.urlVariables.attributes == 'my attributes' + assert result.urlVariables.fields == 'my fields' + assert result.urlVariables.dataNodeSelector == 'my dataNodeSelector' } + + def 'Create url template parameters for GET without all optional parameters.'() { + when: 'Creating URL parameters for GET with null=values where possible' + def result = objectUnderTest.createUrlTemplateParametersForRead( + new Scope(), + null, + null, + null, + new ClassNameIdGetDataNodeSelectorParameter(dataNodeSelector: 'my dataNodeSelector'), + new YangModelCmHandle(dmiServiceName: 'myDmiService'), + new RequestPathParameters(uriLdnFirstPart:'myPathVariable=myPathValue', className: 'myClassName', id:'myId')) + then: 'The url variables contains only a data node selector' + assert result.urlVariables.size() == 1 + assert result.urlVariables.keySet()[0] == 'dataNodeSelector' + } + + def 'Create url template parameters for PUT and PATCH.'() { + when: 'Creating URL parameters for PUT (or PATCH)' + def result = objectUnderTest.createUrlTemplateParametersForWrite( + new YangModelCmHandle(dmiServiceName: 'myDmiService'), + new RequestPathParameters(uriLdnFirstPart:'myPathVariable=myPathValue', className: 'myClassName', id:'myId')) + then: 'the template has the correct correct' + assert result.urlTemplate.toString().startsWith('myDmiService/ProvMnS/v1/myPathVariable=myPathValue/myClassName=myId') + and: 'no url variables have been set' + assert result.urlVariables.isEmpty() + } + + } 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 a0350eaefb..698ff1ac76 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 @@ -21,6 +21,7 @@ package org.onap.cps.integration.functional.ncmp.provmns import org.onap.cps.integration.base.CpsIntegrationSpecBase +import org.onap.cps.ncmp.impl.provmns.model.PatchItem import org.onap.cps.ncmp.impl.provmns.model.ResourceOneOf import org.springframework.http.MediaType @@ -62,12 +63,12 @@ class ProvMnSRestApiSpec extends CpsIntegrationSpecBase{ 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')) + def jsonBody = jsonObjectMapper.asJsonString([new PatchItem(op: 'REMOVE', path: 'someUriLdnFirstPart')]) 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()) + .andExpect(status().isOk()) cleanup: 'deregister CM handles' deregisterCmHandle(DMI1_URL, 'ch-1') }