Implement PATCH for ProvMnS interface with bug fixes 10/142410/7
authorseanbeirne <sean.beirne@est.tech>
Tue, 11 Nov 2025 15:50:01 +0000 (15:50 +0000)
committerseanbeirne <sean.beirne@est.tech>
Thu, 13 Nov 2025 08:59:50 +0000 (08:59 +0000)
-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 <sean.beirne@est.tech>
14 files changed:
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnS.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnsController.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/provmns/ErrorResponseBuilder.java
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnsControllerSpec.groovy
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/CreateOperationDetails.java [moved from cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/OperationDetails.java with 84% similarity]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/PatchOperationsDetails.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutor.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/dmi/DmiRestClient.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/provmns/ParametersBuilder.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/policyexecutor/PolicyExecutorSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/dmi/DmiRestClientIntegrationSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParameterMapperSpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/provmns/ParametersBuilderSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/provmns/ProvMnSRestApiSpec.groovy

index 68b6151..d9840d5 100644 (file)
@@ -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<PatchItem> patchItems
     );
 
 
index 3286dc5..79acf2d 100644 (file)
@@ -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<Object> patchMoi(final HttpServletRequest httpServletRequest, final Resource resource) {
+    public ResponseEntity<Object> patchMoi(final HttpServletRequest httpServletRequest,
+                                           final List<PatchItem> 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());
index 8be88d5..50b99c2 100644 (file)
@@ -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<Object> 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<Object> buildErrorResponseGet(final HttpStatus httpStatus, final String reason) {
index a858f3d..cecf24c 100644 (file)
@@ -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'() {
@@ -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<String, List<OperationEntry>> changeRequest) {}
+public record CreateOperationDetails(String operation,
+                                     String targetIdentifier,
+                                     Map<String, List<OperationEntry>> 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 (file)
index 0000000..d456507
--- /dev/null
@@ -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<Object> operations) {}
index 8dd9881..d4dbe3f 100644 (file)
@@ -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<PatchItem> patchItems) {
+        final List<Object> 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<String, List<OperationEntry>> 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);
     }
 
     /**
index 9939e61..26f4663 100644 (file)
@@ -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<Object> 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<ResponseEntity<Object>> 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<Object> 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<Object> 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<Object> 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<String> 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();
     }
 
index cf85d74..2e34a3b 100644 (file)
 
 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<String> attributes,
-                                                       final List<String> fields,
-                                                       final ClassNameIdGetDataNodeSelectorParameter dataNodeSelector,
-                                                       final YangModelCmHandle yangModelCmHandle) {
+    public UrlTemplateParameters createUrlTemplateParametersForRead(final Scope scope,
+                                                final String filter,
+                                                final List<String> attributes,
+                                                final List<String> 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<String> queryParameterList) {
+        if (queryParameterList != null) {
+            final String queryParameterText = queryParameterList.toString();
+            return queryParameterText.substring(1, queryParameterText.length() - 1);
+        }
+        return "";
     }
-}
\ No newline at end of file
+}
index 63260df..4ad7cba 100644 (file)
@@ -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()
     }
index 8d70591..f5b9835 100644 (file)
@@ -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 (file)
index 0000000..9acd4bc
--- /dev/null
@@ -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'
+    }
+
+}
index 115fe56..6470131 100644 (file)
@@ -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()
+    }
+
+
 }
index a0350ea..698ff1a 100644 (file)
@@ -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')
     }